@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,248 @@
1
+ // C / C++ memory-safety SAST module.
2
+ //
3
+ // Covers the OWASP C/C++ "banned-API" set: classic functions that are
4
+ // unsafe by design and have safer replacements. Patterns are syntactic —
5
+ // no taint analysis. Each rule has an optional `gate(ctx)` predicate that
6
+ // runs against the file/line context to suppress emissions outside the
7
+ // security-relevant context for that rule.
8
+ //
9
+ // Vuln families:
10
+ // - buffer-overflow strcpy, strcat, gets, sprintf (no `_s` / no `n`)
11
+ // - format-string printf/fprintf/syslog with a non-literal format arg
12
+ // - command-injection system(<non-literal>) — userland exec via shell
13
+ // - mem-unsafe memcpy(dst, src, user_size) without bounds check
14
+ // alloca(user_size)
15
+ // - rng-weak rand() / srand(time(NULL)) for security
16
+ // - hardcoded hardcoded user/password in fopen / connect calls
17
+
18
+ import { blankComments } from './_comment-strip.js';
19
+
20
+ // ── context detectors ───────────────────────────────────────────────────────
21
+
22
+ // Files that #include any well-known crypto header — strong signal that
23
+ // rand()/srand() calls in this file are likely security-relevant.
24
+ const _CRYPTO_INCLUDE_RE = /#\s*include\s*[<"](?:openssl\/|sodium|sodium\.h|sodium\/|mbedtls\/|wolfssl\/|crypto\.h|gcrypt\.h|nettle\/|tomcrypt|bcrypt\.h|wincrypt\.h|bearssl|monocypher|s2n|botan)[^>"]*[>"]/i;
25
+
26
+ // Variable names that suggest the rand() output feeds something security-
27
+ // sensitive: tokens, keys, IVs, nonces, salts, session IDs, passwords.
28
+ // `\b` word boundaries so `iv` doesn't match `private`.
29
+ const _CRYPTO_VAR_RE = /\b(?:token|secret|password|passwd|pwd|cookie|session|sid|csrf|challenge|jwt|hmac|signature|sig|apikey|api_key|cryptoKey|cryptokey|encryption_key|cipher|nonce|salt|iv)\w*\b/i;
30
+
31
+ // Sensitive context for `rand()`: line-local evidence that the rand() value
32
+ // flows into a security-named target. Two acceptance conditions:
33
+ // 1. Same-line assignment: `token = ... rand()`, `key[i] = rand()`, etc.
34
+ // 2. Within 2 lines of the rand() call, a crypto-named variable receives
35
+ // a value (handles `unsigned char *buf; for (...) buf[i] = rand();`
36
+ // where `buf` is named cryptographically elsewhere).
37
+ // 3. File includes a crypto header AND window has crypto var hint.
38
+ // File-name signals are removed entirely — they over-fire on test corpora
39
+ // that happen to include "random"/"prng"/"crypto" in file or directory
40
+ // names, and they don't reflect whether THIS particular rand() call feeds
41
+ // crypto.
42
+ function _isCryptoContextRand(ctx) {
43
+ const lines = ctx.raw.split('\n');
44
+ // 5-line forward + 2-line back window from the rand() site. Forward
45
+ // emphasis catches `int *buf = malloc(n); for (i=0;i<n;i++) buf[i] = rand();`
46
+ // where the crypto-named target is declared above and used below.
47
+ const startLine = Math.max(0, ctx.line - 3);
48
+ const endLine = Math.min(lines.length, ctx.line + 5);
49
+ const window = lines.slice(startLine, endLine).join('\n');
50
+ // The rand call itself must appear in the window — sanity, since ctx.line
51
+ // is 1-indexed.
52
+ if (!/\b(?:rand|random|srand)\s*\(/.test(window)) return false;
53
+ // Strong signal: an assignment in the window has a crypto-named LHS and
54
+ // the same line also calls rand/random/srand. We check line-by-line.
55
+ for (const line of window.split('\n')) {
56
+ if (!/\b(?:rand|random|srand)\s*\(/.test(line)) continue;
57
+ // Common shapes:
58
+ // token = rand();
59
+ // key[i] = rand() & 0xff;
60
+ // cookie = rand() % N;
61
+ // buf->token = rand();
62
+ if (/\b(?:token|secret|password|passwd|pwd|cookie|session|sid|csrf|challenge|jwt|hmac|signature|sig|apikey|api_key|nonce|salt|iv|cipher|encryption_key|cryptoKey|cryptokey)\w*\s*(?:\[[^\]]*\]|\.\w+|->\w+)?\s*=/i.test(line)) {
63
+ return true;
64
+ }
65
+ }
66
+ // Medium signal: file includes a crypto header AND any crypto-named
67
+ // identifier appears in the window (suggests flow into the crypto layer
68
+ // even if not on the immediate lines).
69
+ if (_CRYPTO_INCLUDE_RE.test(ctx.raw) && _CRYPTO_VAR_RE.test(window)) return true;
70
+ return false;
71
+ }
72
+
73
+ // Defensive `sizeof(dst)` check on the destination of a strcpy/strcat. If the
74
+ // surrounding 3 lines guard the copy with `if (strlen(src) < sizeof(dst))` or
75
+ // equivalent, we can't say the call is unsafe.
76
+ const _SIZEOF_GUARD_RE = /\bsizeof\s*\(\s*\w+\s*\)|\bstrnlen\s*\(|\bsnprintf\s*\(/;
77
+ function _isStrcpyGuarded(ctx) {
78
+ const lines = ctx.raw.split('\n');
79
+ const start = Math.max(0, ctx.line - 4);
80
+ const window = lines.slice(start, ctx.line).join(' ');
81
+ return _SIZEOF_GUARD_RE.test(window);
82
+ }
83
+
84
+ // Format-string: only fire when the variable holding the format string was
85
+ // not assigned from a string literal earlier in the file.
86
+ function _isPrintfVarLiteral(ctx, varName) {
87
+ if (!varName) return false;
88
+ // Search for `varName = "literal"` or `const char *varName = "literal"` etc.
89
+ const re = new RegExp(`\\b${varName}\\s*=\\s*"`, 'm');
90
+ // Only consider assignments BEFORE the call (positional check).
91
+ const before = ctx.raw.split('\n').slice(0, ctx.line - 1).join('\n');
92
+ return re.test(before);
93
+ }
94
+
95
+ // ── rule table ──────────────────────────────────────────────────────────────
96
+
97
+ const FINDINGS = [
98
+ // Banned string-handling: no upper bound. strcpy/strcat have safer _s
99
+ // variants on Windows and strlcpy on BSD/macOS.
100
+ {
101
+ id: 'cpp-strcpy', severity: 'high', cwe: 'CWE-120', family: 'buffer-overflow',
102
+ re: /\b(strcpy|strcat|gets|stpcpy|sprintf)\s*\(/g,
103
+ vuln: 'Banned API — unbounded string copy/format (potential buffer overflow)',
104
+ remediation: 'Replace with the bounded variant: strcpy → strlcpy / strcpy_s; strcat → strlcat / strcat_s; gets → fgets(buf, sizeof(buf), stdin); sprintf → snprintf(buf, sizeof(buf), "%s", v). The unbounded form will silently overflow on attacker-controlled input.',
105
+ gate: (ctx) => !_isStrcpyGuarded(ctx),
106
+ },
107
+ {
108
+ // printf/warn-family: format string is ARG 1.
109
+ // printf(fmt, ...) ← fmt at position 1
110
+ // vprintf(fmt, ap) ← fmt at position 1
111
+ // warn(fmt, ...) ← BSD libc, fmt at position 1
112
+ // err(fmt, ...) / errx(fmt) ← BSD libc, fmt at position 1
113
+ id: 'cpp-printf-fmt', severity: 'high', cwe: 'CWE-134', family: 'format-string',
114
+ re: /\b(?:printf|vprintf|warn(?:x)?|err(?:x)?)\s*\(\s*([a-zA-Z_]\w*|argv\[\d+\])\s*[,)]/g,
115
+ vuln: 'Format string vulnerability — non-literal format argument',
116
+ remediation: 'Always pass a literal format string: `printf("%s", user_input)` instead of `printf(user_input)`. A user-controlled `%n` / `%s` chain can read or write arbitrary memory.',
117
+ gate: (ctx, m) => !_isPrintfVarLiteral(ctx, m && m[1]),
118
+ },
119
+ {
120
+ // f-family: format string is ARG 2 — first arg is a FILE* or fd.
121
+ // fprintf(FILE*, fmt, ...)
122
+ // dprintf(fd, fmt, ...)
123
+ // vfprintf(FILE*, fmt, ap)
124
+ // vdprintf(fd, fmt, ap)
125
+ // Match: function(<any-arg>, <fmt-var>, ...). The first arg can include
126
+ // nested calls like `getstream()` — match anything up to the first
127
+ // non-nested comma.
128
+ id: 'cpp-fprintf-fmt', severity: 'high', cwe: 'CWE-134', family: 'format-string',
129
+ re: /\b(?:fprintf|dprintf|vfprintf|vdprintf)\s*\(\s*[^,()]*(?:\([^)]*\))?[^,]*,\s*([a-zA-Z_]\w*|argv\[\d+\])\s*[,)]/g,
130
+ vuln: 'Format string vulnerability — non-literal format argument',
131
+ remediation: 'For fprintf/dprintf, the format string is the SECOND argument: pass a literal like `fprintf(stderr, "%s", user_input)` instead of `fprintf(stderr, user_input)`. A user-controlled `%n` / `%s` chain can read or write arbitrary memory.',
132
+ gate: (ctx, m) => !_isPrintfVarLiteral(ctx, m && m[1]),
133
+ },
134
+ {
135
+ // syslog/vsyslog: format string is ARG 2 — first arg is priority (int).
136
+ // syslog(priority, fmt, ...)
137
+ // vsyslog(priority, fmt, ap)
138
+ id: 'cpp-syslog-fmt', severity: 'high', cwe: 'CWE-134', family: 'format-string',
139
+ re: /\b(?:syslog|vsyslog)\s*\(\s*[^,]+,\s*([a-zA-Z_]\w*|argv\[\d+\])\s*[,)]/g,
140
+ vuln: 'Format string vulnerability — non-literal format argument',
141
+ remediation: 'For syslog, the format string is the SECOND argument: pass a literal like `syslog(LOG_INFO, "%s", user_input)` instead of `syslog(LOG_INFO, user_input)`.',
142
+ gate: (ctx, m) => !_isPrintfVarLiteral(ctx, m && m[1]),
143
+ },
144
+ {
145
+ // s-family: format string is ARG 2 for sprintf, ARG 3 for snprintf.
146
+ // sprintf(buf, fmt, ...)
147
+ // snprintf(buf, n, fmt, ...)
148
+ // vsprintf(buf, fmt, ap)
149
+ // vsnprintf(buf, n, fmt, ap)
150
+ id: 'cpp-sprintf-fmt', severity: 'high', cwe: 'CWE-134', family: 'format-string',
151
+ re: /\b(?:s(?:n)?printf|vs(?:n)?printf)\s*\(\s*[^,]+,(?:\s*[^,]+,)?\s*([a-zA-Z_]\w*|argv\[\d+\])\s*[,)]/g,
152
+ vuln: 'Format string vulnerability — non-literal format argument',
153
+ remediation: 'For sprintf/snprintf, the format string is the second/third argument: pass a literal format with placeholders rather than user-controlled data as the format itself.',
154
+ gate: (ctx, m) => !_isPrintfVarLiteral(ctx, m && m[1]),
155
+ },
156
+ {
157
+ id: 'cpp-system', severity: 'critical', cwe: 'CWE-78', family: 'command-injection',
158
+ re: /\bsystem\s*\(\s*(?!["'])\w/g,
159
+ vuln: 'Command Injection — system() with non-literal argument',
160
+ remediation: 'Replace `system(cmd)` with `execve(...)` + fork(), passing the program and arguments as separate strings (no shell interpretation). When using system() with concatenated input, attacker-controlled `; rm -rf /` becomes literal shell.',
161
+ },
162
+ {
163
+ id: 'cpp-popen', severity: 'critical', cwe: 'CWE-78', family: 'command-injection',
164
+ re: /\bpopen\s*\(\s*(?!["'])\w/g,
165
+ vuln: 'Command Injection — popen() with non-literal command',
166
+ remediation: 'popen() invokes the shell. Use a fork()+execve() pattern with pipes instead, or use posix_spawn() with `posix_spawnattr_setflags(...)` and no shell.',
167
+ },
168
+ {
169
+ id: 'cpp-memcpy-usersz', severity: 'high', cwe: 'CWE-787', family: 'mem-unsafe',
170
+ // memcpy(dst, src, var) where var ends in _len/size/count and was assigned from input
171
+ re: /\b(?:memcpy|memmove|bcopy)\s*\(\s*\w+\s*,\s*\w+\s*,\s*\w+(?:_len|_size|_count|Len|Size|Count|len|size|count)\s*\)/g,
172
+ vuln: 'Memory-safety risk — memcpy/memmove with externally-controlled size',
173
+ remediation: 'Validate the size against the destination buffer before copying: `if (n > sizeof(dst)) return -1;`. Better: use std::span (C++20) or use a typed copy that carries length, like strncpy_s with explicit destmax.',
174
+ },
175
+ {
176
+ id: 'cpp-alloca', severity: 'medium', cwe: 'CWE-770', family: 'mem-unsafe',
177
+ re: /\balloca\s*\(/g,
178
+ vuln: 'Stack-allocation with user-controllable size (DoS / stack exhaustion)',
179
+ remediation: 'alloca() allocates on the stack with no fault behaviour — a large or attacker-influenced size crashes the process or jumps the guard page. Use malloc()/free() or std::vector instead.',
180
+ },
181
+ {
182
+ id: 'cpp-rand', severity: 'medium', cwe: 'CWE-338', family: 'weak-rng',
183
+ re: /\b(?:rand|random|srand)\s*\(/g,
184
+ vuln: 'Cryptographically weak PRNG (rand/random/srand)',
185
+ remediation: 'rand() is a linear-congruential generator — predictable from a few outputs. For security use cases (tokens, IVs, salts), use a CSPRNG: getrandom() / RAND_bytes() / std::random_device + std::mt19937_64 seeded from /dev/urandom.',
186
+ // Only fire in plausibly-cryptographic contexts. Outside crypto: rand()
187
+ // is a normal language facility (test data, branch selection, jitter).
188
+ gate: (ctx) => _isCryptoContextRand(ctx),
189
+ },
190
+ {
191
+ id: 'cpp-srand-time', severity: 'high', cwe: 'CWE-338', family: 'weak-rng',
192
+ re: /\bsrand\s*\(\s*time\s*\(\s*(?:NULL|nullptr|0)?\s*\)/g,
193
+ vuln: 'Cryptographic randomness seeded from time() (fully predictable)',
194
+ remediation: 'time() seeds are guessable to within ±1 second. For any security-sensitive RNG, seed from /dev/urandom or use OS-provided CSPRNG (getrandom() / BCryptGenRandom).',
195
+ // Same gate — `srand(time(NULL))` outside a crypto context is just a
196
+ // common (bad) example pattern, not a real vulnerability.
197
+ gate: (ctx) => _isCryptoContextRand(ctx),
198
+ },
199
+ ];
200
+
201
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
202
+
203
+ export function scanCpp(fp, raw) {
204
+ if (!/\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/i.test(fp)) return [];
205
+ if (!raw || raw.length > 500_000) return [];
206
+ const code = blankComments(raw);
207
+ // Skip pure header files that only declare functions / contain typedefs.
208
+ // A header with no function calls is unlikely to be a useful target.
209
+ if (/\.(?:h|hh|hpp|hxx)$/i.test(fp) && !/[A-Za-z_]\w*\s*\([^)]*\)\s*\{/.test(code)) return [];
210
+ const out = [];
211
+ const seen = new Set();
212
+ for (const rule of FINDINGS) {
213
+ const re = new RegExp(rule.re.source, rule.re.flags);
214
+ let m;
215
+ while ((m = re.exec(code))) {
216
+ const line = lineOf(raw, m.index);
217
+ const id = `${rule.id}:${fp}:${line}`;
218
+ if (seen.has(id)) continue;
219
+ seen.add(id);
220
+ // Suppress when the match falls inside a #define macro line — those
221
+ // are often re-declarations / wrappers in the same file.
222
+ const lineText = (raw.split('\n')[line - 1] || '');
223
+ if (/^\s*#\s*define\b/.test(lineText)) continue;
224
+ // Per-rule contextual gate (Action 2). Suppress when the surrounding
225
+ // file/line context shows the call is not security-relevant.
226
+ if (typeof rule.gate === 'function') {
227
+ try {
228
+ if (!rule.gate({ file: fp, raw, line, lineText }, m)) continue;
229
+ } catch { /* gate threw → fail open, keep finding */ }
230
+ }
231
+ out.push({
232
+ id, file: fp, line,
233
+ vuln: rule.vuln,
234
+ severity: rule.severity,
235
+ cwe: rule.cwe,
236
+ stride: rule.family === 'buffer-overflow' || rule.family === 'mem-unsafe' ? 'Tampering'
237
+ : rule.family === 'command-injection' ? 'Elevation of Privilege'
238
+ : rule.family === 'format-string' ? 'Information Disclosure'
239
+ : 'Spoofing',
240
+ snippet: lineText.trim().slice(0, 200),
241
+ remediation: rule.remediation,
242
+ confidence: 0.85,
243
+ parser: 'CPP',
244
+ });
245
+ }
246
+ }
247
+ return out;
248
+ }
@@ -0,0 +1,152 @@
1
+ // C# / .NET SAST module.
2
+ //
3
+ // Narrow, high-signal patterns for ASP.NET (Framework + Core), EF, Razor.
4
+ // Each rule fires ONLY on the unsafe shape and has a safe-shape detector
5
+ // where applicable — keeps precision high and avoids polluting clean repos
6
+ // with low-signal warnings.
7
+ //
8
+ // Covered families:
9
+ // - sql-injection SqlCommand / EF FromSqlRaw with string concat
10
+ // - command-injection Process.Start with UseShellExecute=true and user input
11
+ // - xss Razor Html.Raw with user input; .ToString() bypass
12
+ // - xxe XmlDocument w/o XmlResolver=null; XmlReader settings
13
+ // - insecure-deserialization Newtonsoft.Json TypeNameHandling.All; BinaryFormatter
14
+ // - path-traversal Path.Combine with user input + no canonical check
15
+ // - validate-input-disabled [ValidateInput(false)] attribute (legacy MVC)
16
+
17
+ import { blankComments } from './_comment-strip.js';
18
+
19
+ const RE = {
20
+ // SqlCommand("SELECT … " + var) or new SqlCommand("…" + var, conn)
21
+ // Matches the concat shape inside the constructor's first arg.
22
+ sqlConcat: /\bnew\s+SqlCommand\s*\(\s*["'][^"']*["']\s*\+\s*\w/g,
23
+ // SqlCommand("SELECT " + …) followed later by .CommandText
24
+ sqlCmdText: /\bSqlCommand\s*\([^)]*\)[\s\S]{0,400}?\.\s*CommandText\s*=\s*["'][^"']*["']\s*\+/g,
25
+ // EF: ctx.Users.FromSqlRaw($"…{userInput}…") or .FromSql("…" + userInput)
26
+ efFromSqlInterp: /\.\s*FromSql(?:Raw)?\s*\(\s*\$"[^"]*\{(?!\d)/g,
27
+ efFromSqlConcat: /\.\s*FromSql(?:Raw)?\s*\(\s*["'][^"']*["']\s*\+\s*\w/g,
28
+ // Process.Start with UseShellExecute=true AND a variable Arguments
29
+ procShellTrue: /\bnew\s+ProcessStartInfo\s*\{[^}]*\bUseShellExecute\s*=\s*true[\s\S]{0,500}?\bArguments\s*=\s*[^"'][\w.]/g,
30
+ // Direct Process.Start("cmd.exe", userInput) — the 2-arg form runs through cmd
31
+ procStart2: /\bProcess\.Start\s*\(\s*"cmd(?:\.exe)?"\s*,\s*\w/gi,
32
+ // Razor Html.Raw(userInput) — bypasses encoding
33
+ htmlRaw: /\b(?:Html|@Html)\s*\.\s*Raw\s*\(\s*(?!["'])\s*\w/g,
34
+ // XmlDocument loaded without disabling resolver
35
+ xmlDocLoad: /\bnew\s+XmlDocument\s*\(\s*\)|\bvar\s+\w+\s*=\s*new\s+XmlDocument\b/g,
36
+ // XmlReaderSettings without DtdProcessing=Prohibit
37
+ xmlReaderSettings: /\bnew\s+XmlReaderSettings\s*\(\s*\)/g,
38
+ // Newtonsoft.Json TypeNameHandling.All / Auto / Objects / Arrays
39
+ newtonsoftType: /\bTypeNameHandling\s*=\s*TypeNameHandling\.(?:All|Auto|Objects|Arrays)/g,
40
+ // BinaryFormatter — entire surface is unsafe since .NET 5.
41
+ binaryFormatter: /\bnew\s+BinaryFormatter\s*\(\s*\)/g,
42
+ // [ValidateInput(false)] — ASP.NET MVC legacy bypass for XSS validation
43
+ validateInputFalse: /\[\s*ValidateInput\s*\(\s*false\s*\)\s*\]/g,
44
+ // Path.Combine(... userInput ...) with no canonical / startsWith check
45
+ pathCombine: /\bPath\.Combine\s*\(\s*[^)]*\b(?:Request\.|HttpContext\.|fileName|userInput|input|name|path)\b/gi,
46
+ };
47
+
48
+ // File-level safe-shape detectors. When ANY of these appear in the file the
49
+ // corresponding family is suppressed for the whole file. Mirrors the OWASP
50
+ // Benchmark file-level pattern.
51
+ const SAFE = {
52
+ // Parameterized SQL: ".Parameters.Add(...) " or "@param" placeholder
53
+ sql: /\.\s*Parameters\.\s*Add(?:WithValue)?\s*\(|@\w+\s*[,)]/,
54
+ // XML safe: XmlResolver = null OR DtdProcessing = DtdProcessing.Prohibit
55
+ xml: /\bXmlResolver\s*=\s*null\b|\bDtdProcessing\s*=\s*DtdProcessing\.Prohibit\b|\.\s*XmlResolver\s*=\s*null/,
56
+ // Path-traversal: GetFullPath + StartsWith
57
+ path: /\.\s*GetFullPath\s*\([\s\S]{0,200}?\.\s*StartsWith\s*\(/,
58
+ };
59
+
60
+ const FINDINGS = [
61
+ { id: 'csharp-sql-concat', re: RE.sqlConcat, severity: 'high', cwe: 'CWE-89',
62
+ vuln: 'SQL Injection — SqlCommand string concatenation',
63
+ remediation: 'Use parameterized queries: `var cmd = new SqlCommand("SELECT * FROM users WHERE id = @id", conn); cmd.Parameters.AddWithValue("@id", id);`. Never build SQL via concatenation; the database can\'t tell user data from SQL syntax once they\'re joined.',
64
+ fileSafe: SAFE.sql, family: 'sql-injection' },
65
+ { id: 'csharp-sql-cmdtext', re: RE.sqlCmdText, severity: 'high', cwe: 'CWE-89',
66
+ vuln: 'SQL Injection — SqlCommand.CommandText concatenation',
67
+ remediation: 'Assign a fully parameterized SQL string and use `cmd.Parameters.AddWithValue(...)` for every user-supplied value.',
68
+ fileSafe: SAFE.sql, family: 'sql-injection' },
69
+ { id: 'csharp-ef-fromsql-interp', re: RE.efFromSqlInterp, severity: 'high', cwe: 'CWE-89',
70
+ vuln: 'SQL Injection — EF Core FromSqlRaw with interpolated string',
71
+ remediation: 'Switch to `FromSqlInterpolated($"...")` (EF Core parameterizes interpolation holes automatically) or use `FromSqlRaw("...{0}...", value)` with positional placeholders. `FromSqlRaw($"...{var}...")` defeats the protection by evaluating the f-string first.',
72
+ family: 'sql-injection' },
73
+ { id: 'csharp-ef-fromsql-concat', re: RE.efFromSqlConcat, severity: 'high', cwe: 'CWE-89',
74
+ vuln: 'SQL Injection — EF Core FromSqlRaw with string concatenation',
75
+ remediation: 'Use `FromSqlInterpolated($"... {value}")` or `FromSqlRaw("... {0}", value)` with positional parameters.',
76
+ family: 'sql-injection' },
77
+ { id: 'csharp-proc-shellexec', re: RE.procShellTrue, severity: 'critical', cwe: 'CWE-78',
78
+ vuln: 'Command Injection — Process.Start with UseShellExecute=true and dynamic Arguments',
79
+ remediation: 'Set `UseShellExecute = false` and pass arguments as a `string[]` via `ProcessStartInfo.ArgumentList`. ShellExecute=true routes through cmd.exe / the shell, so any user-controlled metacharacter is interpreted.',
80
+ family: 'command-injection' },
81
+ { id: 'csharp-proc-cmd', re: RE.procStart2, severity: 'critical', cwe: 'CWE-78',
82
+ vuln: 'Command Injection — Process.Start("cmd.exe", userInput)',
83
+ remediation: 'Never invoke cmd.exe with user input as the arguments string. Call the target executable directly via ProcessStartInfo with ArgumentList.',
84
+ family: 'command-injection' },
85
+ { id: 'csharp-htmlraw', re: RE.htmlRaw, severity: 'high', cwe: 'CWE-79',
86
+ vuln: 'XSS — Razor Html.Raw with user input bypasses encoding',
87
+ remediation: '`@Html.Raw(x)` emits `x` without HTML-encoding. Use the default `@x` syntax which auto-encodes, or sanitize first via HtmlSanitizer.NET / AntiXss.GetSafeHtmlFragment.',
88
+ family: 'xss' },
89
+ { id: 'csharp-xmldoc-no-resolver', re: RE.xmlDocLoad, severity: 'high', cwe: 'CWE-611',
90
+ vuln: 'XXE — XmlDocument without XmlResolver=null',
91
+ remediation: 'After `new XmlDocument()`, set `doc.XmlResolver = null;` BEFORE calling `.LoadXml()` or `.Load()`. Default behaviour in older .NET Framework versions resolves external entities.',
92
+ fileSafe: SAFE.xml, family: 'xxe' },
93
+ { id: 'csharp-xmlreader-no-dtd', re: RE.xmlReaderSettings, severity: 'medium', cwe: 'CWE-611',
94
+ vuln: 'XXE — XmlReaderSettings without DtdProcessing=Prohibit',
95
+ remediation: 'Configure `new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }`. `DtdProcessing.Parse` (the .NET Framework default) is the unsafe shape.',
96
+ fileSafe: SAFE.xml, family: 'xxe' },
97
+ { id: 'csharp-newtonsoft-typename', re: RE.newtonsoftType, severity: 'critical', cwe: 'CWE-502',
98
+ vuln: 'Insecure Deserialization — Newtonsoft.Json TypeNameHandling != None',
99
+ remediation: '`TypeNameHandling.All/Auto/Objects/Arrays` allows the payload to specify the .NET type to instantiate, enabling RCE via gadget chains. Set `TypeNameHandling.None` (the default) or migrate to System.Text.Json.',
100
+ family: 'insecure-deserialization' },
101
+ { id: 'csharp-binformatter', re: RE.binaryFormatter, severity: 'critical', cwe: 'CWE-502',
102
+ vuln: 'Insecure Deserialization — BinaryFormatter',
103
+ remediation: 'BinaryFormatter is obsolete and unsafe — Microsoft has marked it deprecated in .NET 5+. Replace with System.Text.Json or DataContractSerializer with KnownTypes set.',
104
+ family: 'insecure-deserialization' },
105
+ { id: 'csharp-validate-input-false', re: RE.validateInputFalse, severity: 'high', cwe: 'CWE-79',
106
+ vuln: 'XSS — [ValidateInput(false)] disables ASP.NET request validation',
107
+ remediation: 'Re-enable request validation (`[ValidateInput(true)]` or remove the attribute) and explicitly HTML-encode any field that must accept tags.',
108
+ family: 'xss' },
109
+ { id: 'csharp-path-combine-user', re: RE.pathCombine, severity: 'high', cwe: 'CWE-22',
110
+ vuln: 'Path Traversal — Path.Combine with user input and no canonical check',
111
+ remediation: 'After `Path.Combine`, call `Path.GetFullPath(joined).StartsWith(Path.GetFullPath(baseDir))` and reject mismatches. Without this, `..\\..\\etc\\passwd` escapes the intended directory.',
112
+ fileSafe: SAFE.path, family: 'path-traversal' },
113
+ ];
114
+
115
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
116
+
117
+ export function scanCSharp(fp, raw) {
118
+ if (!/\.cs$/i.test(fp)) return [];
119
+ if (!raw || raw.length > 500_000) return [];
120
+ const code = blankComments(raw);
121
+ const out = [];
122
+ const seen = new Set();
123
+ for (const rule of FINDINGS) {
124
+ if (rule.fileSafe && rule.fileSafe.test(code)) continue;
125
+ const re = new RegExp(rule.re.source, rule.re.flags);
126
+ let m;
127
+ while ((m = re.exec(code))) {
128
+ const line = lineOf(raw, m.index);
129
+ const id = `${rule.id}:${fp}:${line}`;
130
+ if (seen.has(id)) continue;
131
+ seen.add(id);
132
+ out.push({
133
+ id, file: fp, line,
134
+ vuln: rule.vuln,
135
+ severity: rule.severity,
136
+ cwe: rule.cwe,
137
+ stride: rule.cwe === 'CWE-89' ? 'Tampering'
138
+ : rule.cwe === 'CWE-78' ? 'Elevation of Privilege'
139
+ : rule.cwe === 'CWE-79' ? 'Tampering'
140
+ : rule.cwe === 'CWE-611' ? 'Information Disclosure'
141
+ : rule.cwe === 'CWE-502' ? 'Elevation of Privilege'
142
+ : rule.cwe === 'CWE-22' ? 'Tampering'
143
+ : 'Tampering',
144
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
145
+ remediation: rule.remediation,
146
+ confidence: 0.85,
147
+ parser: 'CSHARP',
148
+ });
149
+ }
150
+ }
151
+ return out;
152
+ }
@@ -0,0 +1,82 @@
1
+ import { blankComments } from './_comment-strip.js';
2
+ // CSRF on state-changing routes.
3
+ //
4
+ // Heuristic: POST / PUT / PATCH / DELETE routes that don't show evidence of
5
+ // CSRF protection in the file or in the project-wide middleware chain.
6
+ //
7
+ // Evidence of protection (per-file or per-route):
8
+ // - csurf / csrf middleware in scope
9
+ // - express-csrf-token / lusca.csrf / fastify-csrf
10
+ // - Flask-WTF CSRFProtect / django.middleware.csrf.CsrfViewMiddleware
11
+ // - Spring CsrfFilter / @CsrfProtected
12
+ // - SameSite=Strict|Lax cookie config
13
+ // - Origin / Referer check
14
+ // - Authorization: Bearer (token auth is CSRF-safe by construction)
15
+ // - methodNotAllowed: state-changing route guarded by API-only header
16
+
17
+ const STATE_CHANGE_RE = {
18
+ express: /\b(?:app|router|express\(\))\s*\.\s*(post|put|patch|delete)\s*\(/gi,
19
+ fastify: /\b(?:fastify|server)\s*\.\s*(post|put|patch|delete)\s*\(/gi,
20
+ flask: /@(?:app|bp|blueprint)\s*\.\s*route\s*\([^)]*methods\s*=\s*\[[^\]]*['"](POST|PUT|PATCH|DELETE)['"]/gi,
21
+ django: /\b(?:require_POST|require_http_methods\s*\(\s*\[[^\]]*['"](POST|PUT|PATCH|DELETE)['"])/gi,
22
+ fastapi: /@\w+\s*\.\s*(post|put|patch|delete)\s*\(/gi,
23
+ spring: /@(PostMapping|PutMapping|PatchMapping|DeleteMapping)\b/g,
24
+ };
25
+
26
+ const CSRF_DEFENCE_RE = /\b(?:csurf|csrfProtection|csrf\(\)|express-csrf-token|lusca\.csrf|fastify-csrf|CSRFProtect|csrf_protect|CsrfViewMiddleware|CsrfFilter|@CsrfProtected|sameSite\s*[:=]\s*['"](?:Strict|Lax)['"]|origin\s*===|referer\s*===|Origin\s*===|Referer\s*===)/i;
27
+
28
+ const TOKEN_AUTH_RE = /\b(?:Authorization\s*:\s*Bearer|\.startsWith\s*\(\s*['"`]Bearer|req\.headers\.authorization|request\.headers\.get\(\s*['"`]authorization|bearer\s+token|x-api-key|verifyJWT|jwt\.verify|jsonwebtoken)/i;
29
+
30
+ const TEST_FILE_RE = /(?:^|\/)(?:tests?|__tests__|specs?|test|fixtures)\//i;
31
+
32
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
33
+
34
+ export function scanCSRF(fp, raw) {
35
+ if (!raw || raw.length > 500_000) return [];
36
+ if (TEST_FILE_RE.test(fp)) return [];
37
+ const ext = (fp.match(/\.([a-z]+)$/i) || [])[1] || '';
38
+ let langSel = null;
39
+ if (/^(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(ext)) langSel = ['express', 'fastify'];
40
+ else if (ext === 'py') langSel = ['flask', 'django', 'fastapi'];
41
+ else if (ext === 'java' || ext === 'kt') langSel = ['spring'];
42
+ if (!langSel) return [];
43
+
44
+ const code = blankComments(raw, ext === 'py' ? 'py' : undefined);
45
+ // Project-wide-ish: if the file shows CSRF defence anywhere or only handles
46
+ // token-authenticated routes, we suppress.
47
+ const csrfInScope = CSRF_DEFENCE_RE.test(code);
48
+ const tokenAuthInScope = TOKEN_AUTH_RE.test(code);
49
+ if (csrfInScope || tokenAuthInScope) return [];
50
+
51
+ const findings = [];
52
+ const seen = new Set();
53
+ for (const sel of langSel) {
54
+ const re = new RegExp(STATE_CHANGE_RE[sel].source, STATE_CHANGE_RE[sel].flags);
55
+ let m;
56
+ while ((m = re.exec(code))) {
57
+ const line = lineOf(raw, m.index);
58
+ const id = `csrf:${fp}:${line}`;
59
+ if (seen.has(id)) continue;
60
+ seen.add(id);
61
+ // Check ±15 lines for inline defence (one-off route-level guard).
62
+ // Use the comment-blanked text — comments like "// no csurf yet" must
63
+ // not satisfy the defence check.
64
+ const windowLines = code.split('\n').slice(Math.max(0, line - 16), line + 15).join(' ');
65
+ if (CSRF_DEFENCE_RE.test(windowLines) || TOKEN_AUTH_RE.test(windowLines)) continue;
66
+ const method = (m[1] || (sel === 'spring' ? m[1] : '')).toUpperCase();
67
+ findings.push({
68
+ id,
69
+ file: fp, line,
70
+ vuln: `Missing CSRF protection on state-changing route (${method || 'POST/PUT/PATCH/DELETE'})`,
71
+ severity: 'high',
72
+ cwe: 'CWE-352',
73
+ stride: 'Tampering',
74
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
75
+ remediation: 'For session-cookie auth: add the framework\'s CSRF middleware (`csurf` for Express, `Flask-WTF CSRFProtect` for Flask, the built-in `CsrfViewMiddleware` for Django, `CsrfFilter` for Spring) and require a token in every state-changing form. For pure token auth (Authorization: Bearer), set `SameSite=Strict` on the session cookie or drop the cookie entirely. For SPAs: use a double-submit cookie or per-request token from the server.',
76
+ parser: 'CSRF',
77
+ confidence: 0.65,
78
+ });
79
+ }
80
+ }
81
+ return findings;
82
+ }