@clear-capabilities/agentic-security-scanner 0.86.0 → 0.118.2

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 (120) hide show
  1. package/CHANGELOG.md +404 -0
  2. package/dist/178.index.js +1 -1
  3. package/dist/384.index.js +1 -1
  4. package/dist/637.index.js +1 -1
  5. package/dist/838.index.js +1 -1
  6. package/dist/agentic-security.mjs +84 -84
  7. package/dist/agentic-security.mjs.sha256 +1 -1
  8. package/package.json +12 -6
  9. package/src/.agentic-security/dpia.md +26 -0
  10. package/src/.agentic-security/pqc-migration-plan.json +80 -0
  11. package/src/.agentic-security/pqc-migration-plan.md +32 -0
  12. package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
  13. package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
  14. package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  15. package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
  16. package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
  17. package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
  18. package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
  19. package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  20. package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
  21. package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
  22. package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
  23. package/src/.agentic-security/threat-model.json +3702 -0
  24. package/src/.agentic-security/threat-model.md +118 -0
  25. package/src/dataflow/.agentic-security/dpia.md +26 -0
  26. package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +6 -0
  27. package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  28. package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
  29. package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +6 -0
  30. package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
  31. package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
  32. package/src/dataflow/.agentic-security/threat-model.json +161 -0
  33. package/src/dataflow/.agentic-security/threat-model.md +43 -0
  34. package/src/dataflow/CLAUDE.md +17 -5
  35. package/src/dataflow/catalog.js +9 -0
  36. package/src/dataflow/engine.js +30 -4
  37. package/src/dataflow/index.js +8 -0
  38. package/src/dataflow/proof-gate.js +95 -0
  39. package/src/dataflow/summaries.js +44 -5
  40. package/src/engine.js +199 -5
  41. package/src/ir/.agentic-security/dpia.md +26 -0
  42. package/src/ir/.agentic-security/exploit-bundles.json +77 -0
  43. package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
  44. package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +6 -0
  45. package/src/ir/.agentic-security/threat-model.json +143 -0
  46. package/src/ir/.agentic-security/threat-model.md +39 -0
  47. package/src/ir/CLAUDE.md +1 -0
  48. package/src/ir/callgraph.js +12 -1
  49. package/src/ir/tree-sitter-loader.js +108 -0
  50. package/src/posture/.agentic-security/dpia.md +1 -1
  51. package/src/posture/.agentic-security/pqc-migration-plan.json +1 -1
  52. package/src/posture/.agentic-security/pqc-migration-plan.md +1 -1
  53. package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +6 -0
  54. package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  55. package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
  56. package/src/posture/.agentic-security/threat-model.json +3 -3
  57. package/src/posture/.agentic-security/threat-model.md +1 -1
  58. package/src/posture/corpus-status.js +74 -0
  59. package/src/posture/coverage-report.js +113 -0
  60. package/src/posture/holdout-eval.js +57 -0
  61. package/src/posture/router.js +33 -3
  62. package/src/report/.agentic-security/dpia.md +26 -0
  63. package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +6 -0
  64. package/src/report/.agentic-security/threat-model.md +1 -1
  65. package/src/report/index.js +17 -0
  66. package/src/sast/.agentic-security/dpia.md +26 -0
  67. package/src/sast/.agentic-security/pqc-migration-plan.json +65 -0
  68. package/src/sast/.agentic-security/pqc-migration-plan.md +30 -0
  69. package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +6 -0
  70. package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
  71. package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +6 -0
  72. package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
  73. package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  74. package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +6 -0
  75. package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
  76. package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +6 -0
  77. package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
  78. package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
  79. package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
  80. package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +6 -0
  81. package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +6 -0
  82. package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  83. package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
  84. package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +6 -0
  85. package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +6 -0
  86. package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +6 -0
  87. package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
  88. package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +6 -0
  89. package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +6 -0
  90. package/src/sast/.agentic-security/threat-model.json +816 -0
  91. package/src/sast/.agentic-security/threat-model.md +91 -0
  92. package/src/sast/CLAUDE.md +9 -3
  93. package/src/sast/code-injection-multilang.js +117 -0
  94. package/src/sast/crypto-protocol.js +43 -1
  95. package/src/sast/csharp-structural.js +65 -0
  96. package/src/sast/csharp.js +3 -0
  97. package/src/sast/csrf.js +24 -3
  98. package/src/sast/csv-injection.js +84 -0
  99. package/src/sast/frontend-hygiene.js +106 -0
  100. package/src/sast/go-structural.js +60 -0
  101. package/src/sast/java-structural.js +81 -0
  102. package/src/sast/js-framework-structural.js +115 -0
  103. package/src/sast/kotlin.js +72 -0
  104. package/src/sast/ldap-injection.js +73 -17
  105. package/src/sast/open-redirect.js +47 -5
  106. package/src/sast/php.js +35 -0
  107. package/src/sast/python-structural.js +83 -0
  108. package/src/sast/redos-nfa.js +75 -0
  109. package/src/sast/response-splitting.js +48 -19
  110. package/src/sast/ruby.js +38 -0
  111. package/src/sast/secret-concat.js +60 -0
  112. package/src/sast/ssrf-cloud-metadata.js +13 -0
  113. package/src/sast/stored-taint.js +88 -0
  114. package/src/sast/tree-sitter-sinks.js +62 -0
  115. package/src/sast/weak-password-hash.js +28 -0
  116. package/src/sast/weak-randomness.js +46 -5
  117. package/src/sast/wrong-context-sanitizer.js +94 -0
  118. package/src/sast/xpath-injection.js +30 -6
  119. package/src/sast/xss-reflected-multilang.js +116 -0
  120. package/src/sast/xxe.js +67 -0
@@ -0,0 +1,91 @@
1
+ # Threat Model (auto-generated)
2
+
3
+ Generated by agentic-security on 2026-06-01.
4
+
5
+ This threat model is derived from static analysis of the current codebase and is regenerated on every scan. It is intended as a working artifact, not a finished compliance document.
6
+
7
+ ## Entities + boundaries
8
+
9
+ ```mermaid
10
+ flowchart TB
11
+ subgraph External
12
+ route_GET___________________________________________________["http-route: GET
13
+ parser: "]
14
+ queue_sensitive_data_logging_js_67["queue-consumer: sensitive-data-logging.js:67"]
15
+ end
16
+ subgraph Application
17
+ end
18
+ External --> route_GET___________________________________________________
19
+ External --> queue_sensitive_data_logging_js_67
20
+ ```
21
+
22
+ ## Assets
23
+
24
+
25
+ ## STRIDE threats
26
+
27
+ ### Tampering (31)
28
+
29
+ - [medium] **crypto-tls-no-verify** (CWE-295) at `crypto-protocol.js:105` — TLS certificate verification disabled — MITM-vulnerable
30
+ - [medium] **crypto-tls-no-verify** (CWE-295) at `crypto-protocol.js:108` — TLS certificate verification disabled — MITM-vulnerable
31
+ - [medium] **crypto-tls-no-verify** (CWE-295) at `crypto-protocol.js:114` — TLS certificate verification disabled — MITM-vulnerable
32
+ - [medium] **crypto-tls-no-verify** (CWE-295) at `crypto-protocol.js:116` — TLS certificate verification disabled — MITM-vulnerable
33
+ - [medium] **crypto-tls-no-verify** (CWE-295) at `crypto-protocol.js:118` — TLS certificate verification disabled — MITM-vulnerable
34
+ - [medium] **eth-sign-used** (CWE-345) at `dapp-frontend.js:74` — eth_sign — signs arbitrary 32-byte hash with no prefix (the "death sign")
35
+ - [medium] **spec-drift** (CWE-770) at `rate-limit.js:34` — Spec drift — _hasRateLimit() claims rate-limit-impl but body lacks expected evidence
36
+ - [medium] **spec-drift** (CWE-770) at `rate-limit.js:77` — Spec drift — scanRateLimit() claims rate-limit-impl but body lacks expected evidence
37
+ - [low] **idor** (CWE-347) at `authz.js:33` — AuthZ: jwt.verify called without algorithms allow-list
38
+ - [low] **cache-poisoning** (CWE-349) at `cache-poisoning.js:20` — Cache Poisoning — X-Forwarded-Host reflected from request to response
39
+ - [low] **client-side-eval-with-dynamic-input** (CWE-95) at `client-side.js:135` — Client-Side — eval with Dynamic Input
40
+ - [low] **client-side-eval-with-dynamic-input** (CWE-95) at `client-side.js:139` — Client-Side — eval with Dynamic Input
41
+ - [low] **client-side-eval-with-dynamic-input** (CWE-95) at `client-side.js:140` — Client-Side — eval with Dynamic Input
42
+ - [low] **idor** (CWE-347) at `crypto-protocol.js:374` — AuthZ: jwt.verify called without algorithms allow-list
43
+ - [low] **idor** (CWE-347) at `crypto-protocol.js:375` — AuthZ: jwt.verify called without algorithms allow-list
44
+ - [low] **crypto-tls-version** (CWE-326) at `crypto-protocol.js:67` — TLS configured to accept versions below TLS 1.2
45
+ - [low] **crypto-tls-version** (CWE-326) at `crypto-protocol.js:69` — TLS configured to accept versions below TLS 1.2
46
+ - [low] **crypto-jwt-key-confusion** (CWE-345) at `crypto-protocol.js:374` — jwt.verify called without algorithms allowlist — algorithm-confusion attack
47
+ - [low] **prompt-template-user-input-interpolated-** (CWE-1336) at `k8s-admission.js:139` — Prompt Template: user input interpolated into prompt string without isolation
48
+ - [low] **llm-app-security** (CWE-1427) at `llm-app.js:93` — Prompt Injection — system prompt concatenated with user input
49
+ - [low] **llm-app-security** (CWE-1427) at `llm-app.js:136` — RAG Injection — vector-store result flows into LLM prompt without sanitization
50
+ - [low] **llm-app-security** (CWE-1427) at `llm-app.js:137` — RAG Injection — vector-store result flows into LLM prompt without sanitization
51
+ - [low] **prompt-injection-system-prompt-built-fro** (CWE-1427) at `llm-owasp.js:180` — Prompt Injection — system prompt built from concatenated/interpolated content (LLM01)
52
+ - [low] **prompt-injection-system-prompt-built-fro** (CWE-1427) at `llm-owasp.js:181` — Prompt Injection — system prompt built from concatenated/interpolated content (LLM01)
53
+ - [low] **prompt-injection-system-prompt-built-fro** (CWE-1427) at `llm-owasp.js:182` — Prompt Injection — system prompt built from concatenated/interpolated content (LLM01)
54
+ - … and 6 more
55
+
56
+ ### Information Disclosure (17)
57
+
58
+ - [low] **crypto-weak-cipher** (CWE-327) at `crypto-protocol.js:141` — Weak symmetric cipher in use (DES / 3DES / RC4 / Blowfish / NULL / EXPORT)
59
+ - [low] **crypto-weak-cipher** (CWE-327) at `crypto-protocol.js:143` — Weak symmetric cipher in use (DES / 3DES / RC4 / Blowfish / NULL / EXPORT)
60
+ - [low] **crypto-weak-cipher** (CWE-327) at `crypto-protocol.js:145` — Weak symmetric cipher in use (DES / 3DES / RC4 / Blowfish / NULL / EXPORT)
61
+ - [low] **crypto-weak-cipher** (CWE-327) at `crypto-protocol.js:149` — Weak symmetric cipher in use (DES / 3DES / RC4 / Blowfish / NULL / EXPORT)
62
+ - [low] **pqc-migration** (CWE-327) at `post-quantum-crypto.js:136` — Pre-quantum RSA (rsa-encrypt) — replace with ML-KEM-768 before CRQC arrives
63
+ - [low] **zip-slip** (CWE-22) at `zip-slip.js:192` — Zip Slip: archive entry.path written to filesystem without sanitization
64
+ - [low] **ssrf** (CWE-918) at `csharp-structural.js:60` — SSRF: explicit reference to cloud instance-metadata endpoint
65
+ - [low] **ssrf** (CWE-918) at `go-extended.js:39` — SSRF: explicit reference to cloud instance-metadata endpoint
66
+ - [low] **ssrf** (CWE-918) at `java-structural.js:76` — SSRF: explicit reference to cloud instance-metadata endpoint
67
+ - [low] **ssrf** (CWE-918) at `js-framework-structural.js:69` — SSRF: explicit reference to cloud instance-metadata endpoint
68
+ - [low] **ssrf** (CWE-918) at `kotlin.js:136` — SSRF: explicit reference to cloud instance-metadata endpoint
69
+ - [low] **ssrf** (CWE-918) at `php.js:111` — SSRF: explicit reference to cloud instance-metadata endpoint
70
+ - [low] **ssrf** (CWE-918) at `python-sinks.js:186` — SSRF: explicit reference to cloud instance-metadata endpoint
71
+ - [low] **ssrf** (CWE-918) at `ssrf-cloud-metadata.js:61` — SSRF: explicit reference to cloud instance-metadata endpoint
72
+ - [low] **ssrf** (CWE-918) at `ssrf-cloud-metadata.js:86` — SSRF: explicit reference to cloud instance-metadata endpoint
73
+ - [low] **ssrf** (CWE-918) at `ssrf-cloud-metadata.js:15` — SSRF: explicit reference to cloud instance-metadata endpoint
74
+ - [low] **crypto-weak-hash** (CWE-327) at `weak-password-hash.js:45` — Weak hash algorithm (MD5 / SHA-1 / MD2 / MD4) used
75
+
76
+ ### Elevation of Privilege (12)
77
+
78
+ - [low] **llm-app-security** (CWE-502) at `llm-app.js:163` — Untrusted Model Load — model file loaded from a non-literal path
79
+ - [low] **llm-app-security** (CWE-502) at `model-load.js:45` — Untrusted Model Load — model file loaded from a non-literal path
80
+ - [low] **ssrf** (CWE-918) at `csharp-structural.js:60` — SSRF: explicit reference to cloud instance-metadata endpoint
81
+ - [low] **ssrf** (CWE-918) at `go-extended.js:39` — SSRF: explicit reference to cloud instance-metadata endpoint
82
+ - [low] **ssrf** (CWE-918) at `java-structural.js:76` — SSRF: explicit reference to cloud instance-metadata endpoint
83
+ - [low] **ssrf** (CWE-918) at `js-framework-structural.js:69` — SSRF: explicit reference to cloud instance-metadata endpoint
84
+ - [low] **ssrf** (CWE-918) at `kotlin.js:136` — SSRF: explicit reference to cloud instance-metadata endpoint
85
+ - [low] **ssrf** (CWE-918) at `php.js:111` — SSRF: explicit reference to cloud instance-metadata endpoint
86
+ - [low] **ssrf** (CWE-918) at `python-sinks.js:186` — SSRF: explicit reference to cloud instance-metadata endpoint
87
+ - [low] **ssrf** (CWE-918) at `ssrf-cloud-metadata.js:61` — SSRF: explicit reference to cloud instance-metadata endpoint
88
+ - [low] **ssrf** (CWE-918) at `ssrf-cloud-metadata.js:86` — SSRF: explicit reference to cloud instance-metadata endpoint
89
+ - [low] **ssrf** (CWE-918) at `ssrf-cloud-metadata.js:15` — SSRF: explicit reference to cloud instance-metadata endpoint
90
+
91
+ ## Attack trees
@@ -13,11 +13,17 @@ SAST detector modules. Each file exports one or more `scan*()` functions returni
13
13
 
14
14
  ## What lives here, by category
15
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`.
16
+ **Language-specific** — `cpp.js`, `csharp.js`, `csharp-structural.js` (regex structural: hardcoded-secret incl. split-concat + guarded SSRF — complements the flow-based `csharp.js`), `dart-flutter.js`, `go-extended.js`, `java-deserialization.js`, `java-structural.js` (regex structural SQLi/cmdi/path/SSRF via concat, with path/SSRF guards — complements the AST/flow Java modules), `kotlin.js` (Kotlin idioms + **taint-independent structural injection** detectors: SQLi/cmdi/path via string template/concat, SSRF (guarded), XXE (insecure XML config), ObjectInputStream deser — closes the corpus Kotlin FNs where a tainted-by-convention param has no in-file source), `php.js` (+ structural SQLi/cmdi via concat/`$`-interp: DB::raw/whereRaw, shell_exec/exec; structural path traversal: readfile/file_get_contents/fopen with concat), `python-sinks.js`, `ruby.js` (+ structural ActiveRecord SQLi via `#{}`/concat, backtick/system cmdi, and File/IO path traversal via interpolation/concat), `rust.js`, `solidity.js`, `swift.js`, `xxe.js` (CWE-611 for Java/Python **and** PHP/Go/Ruby — each non-JVM stack is XXE-safe by default, so it flags the explicit external-entity opt-in: PHP `LIBXML_NOENT`/`LIBXML_DTDLOAD`, Go `xml.Decoder` `Strict=false`/custom `Entity`, Ruby Nokogiri `noent`/`dtdload`/`replace_entities`; default-safe parses don't match).
17
+
18
+ > **Structural-detector pattern (Tier 1 recall).** `kotlin.js`/`ruby.js`/`php.js` carry taint-independent rules: a dangerous sink built with string interpolation/concat is the injection shape regardless of variable names. This closes corpus FNs where a value is routed through a local var (`params[:x]` → `where("…#{x}")`) so the taint engine sees no source. Keep them high-precision: parameterized / array-form / literal variants must NOT match. Verify on the cve-replay `pre/`+`post/` pairs.
19
+
20
+ **Tree-sitter (long-tail languages, opt-in)** — `tree-sitter-sinks.js` (roadmap #8). AST-accurate detectors for languages with no first-class IR parser (rust/solidity/cpp/go/swift/dart), via `../ir/tree-sitter-loader.js`. Gated behind `AGENTIC_SECURITY_TREE_SITTER=1` and the **optional** `web-tree-sitter` + `tree-sitter-wasms` deps (ABI-pinned 0.20.8 ↔ 0.1.13, marked `--external` so they're never bundled). Degrades to a no-op when the flag or deps are absent. First rule: Rust shell-spawn command injection (CWE-78). Anchoring on real AST nodes means comments/strings can't false-match.
17
21
 
18
22
  **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
23
 
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`.
24
+ **Framework structural (taint-independent, JS/Py recall)** — `js-framework-structural.js` (Express/Koa/NestJS/TypeORM: SQLi via `.query`/`.execute` concat-template, koa-send path, `ctx.body` XSS, HttpService SSRF, deep-merge prototype pollution) `python-structural.js` (Flask `render_template_string` XSS/SSTI, Django `.raw`/`.extra` + `cursor.execute` SQLi — robust string-literal matching spans embedded quotes like `"… name = '" + x`; `open()`/`send_file` path traversal via concat/f-string, CWE-22 deferring to `dropGuardedFindings`), and `go-structural.js` (db query + `fmt.Sprintf`/concat SQLi, `os.Open` + concat/Sprintf path traversal). High precision: parameterized/escaped/`{{ }}`-Jinja/`%s`-placeholder forms do NOT match; SSRF/path findings defer to `engine.js dropGuardedFindings` (which also drops a reflected-XSS finding when the reflected value passed through a captured HTML escaper — a *discarded* `escapeHtml(s);` does not count — and skips already-`isSanitized` findings so the suppression pipeline keeps its bookkeeping).
25
+
26
+ **Cross-cutting vuln classes** — `authz.js`, `csrf.js` (POST/PUT/PATCH/DELETE state-changing routes without CSRF defence; defence-aware suppression covers Express/Fastify/Flask/Django/FastAPI/Spring/Symfony **and Go (gin/echo/mux), Rails routes, ASP.NET MVC** — recognizes `gorilla/csrf`/`protect_from_forgery`/`[ValidateAntiForgeryToken]` defences and exempts token-auth (`[ApiController]`, Bearer scheme); bare ASP.NET `[Authorize]` still flags as cookie auth is CSRF-vulnerable), `code-injection-multilang.js` (CWE-94 for Java/C#/Go/Kotlin — dynamic code/expression evaluators on a NON-LITERAL argument: javax.script `eval`, GroovyShell, Spring SpEL `parseExpression`, MVEL/OGNL, Roslyn `CSharpScript`, `DataTable.Compute`, yaegi `interp.Eval`, `text/template` Parse of a user-controlled body; literal arguments don't match. JS/Python/Ruby eval stay with the flow engine + per-language modules), `csv-injection.js` (formula injection into spreadsheet cells, CWE-1236), `secret-concat.js` (language-agnostic hardcoded-secret SPLIT across concatenated literals — `'AKIA' + 'IOSF…'` / `'ghp' + '_…'` / `'sk' + '_live_…'` — reassembled and matched against provider prefixes; complements the contiguous-token secrets scanner and the C#-only split-concat rule), `host-header.js`, `jndi.js`, `jwt-exp.js`, `ldap-injection.js` (CWE-90 across JS/Java/Python **and** PHP/Go/C#/Ruby/Kotlin — filter built by concat/interpolation; an inline call-guard and a file-level escape-API guard suppress `ldap_escape`/`EscapeFilter`/`escape_filter_chars`/`Net::LDAP::Filter`/`EqualityFilter` forms), `xpath-injection.js` (CWE-643 across Java/Python/JS **and** PHP/Go/Ruby/C#/Kotlin — XPath expression built by concat/interpolation: `DOMXPath->query`, `SelectNodes`, Nokogiri `.xpath`, htmlquery/xmlpath, `XPath.compile`; embedded-quote-tolerant literal matching; parameterized/variable-bound APIs and static literals don't match), `mass-assignment.js`, `mutation-xss.js`, `nosql-injection.js`, `prototype-pollution.js`, `response-splitting.js` (CWE-113 CRLF/header injection across JS/Python/Java/PHP/Go/Ruby/C#/Kotlin — a response header value set from a request source without stripping CR/LF; recognizes CRLF-strip sanitizers — `.replace(/[\r\n]/)`, chained `.replace("\r")`, Ruby `gsub`/`delete`, Go `strings.NewReplacer`, PHP `str_replace` — and a request-scope param heuristic for the JVM/C# single-file shape), `ssrf-cloud-metadata.js`, `xss-reflected-multilang.js` (cross-language reflected XSS for Go/Ruby/PHP/C#/Kotlin/Java — user input written into an HTML response via concat/interpolation, e.g. Java servlet `response.getWriter().write("<…" + q)`, with a per-language escaper exclusion so `htmlspecialchars`/`HtmlEncode`/`template.HTMLEscapeString`/ERB `<%= %>`/OWASP `Encode.forHtml` forms don't match; JS/Python XSS stays with the flow engine + framework structural detectors), `stored-taint.js` (second-order / stored injection — **opt-in** via `AGENTIC_SECURITY_STORED_TAINT=1`), `toctou.js`, `wrong-context-sanitizer.js` (HTML-entity encoder used in a URL context — wrong-context output encoding, CWE-79), `zip-slip.js`.
21
27
 
22
28
  **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
29
 
@@ -25,7 +31,7 @@ SAST detector modules. Each file exports one or more `scan*()` functions returni
25
31
 
26
32
  **Agent-of-agent / Claude Code hardening** — `claude-hook-injection.js`, `claude-md-prompt-injection.js`, `claude-settings.js`.
27
33
 
28
- **Auth / web** — `auth-provider.js`, `client-side.js`.
34
+ **Auth / web** — `auth-provider.js`, `client-side.js`, `frontend-hygiene.js` (reverse tabnabbing CWE-1022, missing Subresource Integrity CWE-353, Angular `bypassSecurityTrust*` sanitizer-bypass CWE-79).
29
35
 
30
36
  **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
37
 
@@ -0,0 +1,117 @@
1
+ // Cross-language code-injection detector (CWE-94) — Java / C# / Go / Kotlin.
2
+ //
3
+ // The JS/Python/Ruby eval-family sinks are covered by the flow engine + the
4
+ // per-language modules. The compiled second-tier languages had no CWE-94
5
+ // coverage at all. This module flags the canonical dynamic-code-execution
6
+ // sinks in those languages when the evaluated argument is NON-LITERAL (a
7
+ // variable or expression) — a literal `eval("1+1")` is not injection.
8
+ //
9
+ // Sinks covered:
10
+ // Java/Kotlin (JVM):
11
+ // - javax.script ScriptEngine.eval(<expr>)
12
+ // - GroovyShell().evaluate(<expr>) / GroovyShell.parse
13
+ // - Spring SpEL: (Spel)ExpressionParser().parseExpression(<expr>)
14
+ // - MVEL.eval / OGNL: Ognl.getValue / OgnlUtil
15
+ // C#:
16
+ // - Roslyn: CSharpScript.EvaluateAsync/RunAsync(<expr>)
17
+ // - DataTable().Compute(<expr>, …) (expression evaluator → injection)
18
+ // Go:
19
+ // - yaegi/gomacro interpreter: interp.Eval(<expr>)
20
+ // - text/template|html/template Parse(<expr>) of a NON-LITERAL template
21
+ // (user-controlled template body → action/code execution)
22
+ //
23
+ // Precision: the matched argument must not be a string literal. Escaped/safe
24
+ // constructs (a literal template, a constant expression) don't match.
25
+
26
+ import { blankComments } from './_comment-strip.js';
27
+
28
+ const lineOf = (raw, idx) => raw.substring(0, idx).split('\n').length;
29
+
30
+ // A non-literal argument: an identifier / member / call expression — i.e. NOT
31
+ // a bare string/char/number literal as the whole argument. Used as a trailing
32
+ // assertion `(?!\s*STRING\s*\))` after the opening paren.
33
+ const STR_LIT = String.raw`(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|@?"[^"]*")`;
34
+
35
+ const REMEDIATION =
36
+ 'Never evaluate runtime-built code. Replace the dynamic evaluator with an explicit ' +
37
+ 'dispatch table / allow-list of operations, a sandboxed expression library with no ' +
38
+ 'I/O and a fixed grammar, or precompiled templates whose body is never user-controlled. ' +
39
+ 'For Spring SpEL use a SimpleEvaluationContext (no type/bean access); for Go templates ' +
40
+ 'keep the template body static and pass values as data, never Parse user input.';
41
+
42
+ // Per-language sink patterns. Each is built so the argument is captured and a
43
+ // negative lookahead rejects a pure string-literal argument.
44
+ function patternsFor(lang) {
45
+ // `(?!\s*<str>\s*[),])` — reject when the FIRST argument is a bare literal.
46
+ const notLiteral = String.raw`(?!\s*${STR_LIT}\s*[),])`;
47
+ if (lang === 'java' || lang === 'kt') {
48
+ return [
49
+ // ScriptEngine.eval(expr) — but not eval(reader) literal; require non-literal.
50
+ { key: 'script-eval', re: new RegExp(String.raw`\.\s*eval\s*\(\s*${notLiteral}`, 'g'),
51
+ gate: /\b(?:ScriptEngine|ScriptEngineManager|getEngineByName|javax\.script|NashornScriptEngine)\b/ },
52
+ { key: 'groovy', re: new RegExp(String.raw`\bGroovyShell\s*\([^)]*\)\s*\.\s*(?:evaluate|parse)\s*\(\s*${notLiteral}`, 'g') },
53
+ { key: 'groovy2', re: new RegExp(String.raw`\b(?:GroovyShell|GroovyClassLoader)\b[\s\S]{0,120}?\.\s*(?:evaluate|parse|parseClass)\s*\(\s*${notLiteral}`, 'g') },
54
+ { key: 'spel', re: new RegExp(String.raw`\bparseExpression\s*\(\s*${notLiteral}`, 'g'),
55
+ gate: /\b(?:SpelExpressionParser|ExpressionParser|org\.springframework\.expression)\b/ },
56
+ { key: 'mvel', re: new RegExp(String.raw`\bMVEL\s*\.\s*(?:eval|evalToString|compileExpression|executeExpression)\s*\(\s*${notLiteral}`, 'g') },
57
+ { key: 'ognl', re: new RegExp(String.raw`\bOgnl\s*\.\s*(?:getValue|setValue|parseExpression)\s*\(\s*${notLiteral}`, 'g') },
58
+ ];
59
+ }
60
+ if (lang === 'cs') {
61
+ return [
62
+ { key: 'roslyn', re: new RegExp(String.raw`\bCSharpScript\s*\.\s*(?:EvaluateAsync|RunAsync|Create)\s*\(\s*${notLiteral}`, 'g') },
63
+ { key: 'datatable-compute', re: new RegExp(String.raw`\.\s*Compute\s*\(\s*${notLiteral}`, 'g'),
64
+ gate: /\b(?:DataTable|DataColumn|DataView)\b/ },
65
+ ];
66
+ }
67
+ if (lang === 'go') {
68
+ return [
69
+ // yaegi / gomacro interpreter eval of a non-literal.
70
+ { key: 'interp-eval', re: new RegExp(String.raw`\.\s*Eval\s*\(\s*${notLiteral}`, 'g'),
71
+ gate: /\b(?:interp|yaegi|gomacro|Interpreter)\b/ },
72
+ // template Parse of a non-literal template body (user-controlled template).
73
+ { key: 'template-parse', re: new RegExp(String.raw`\.\s*Parse\s*\(\s*${notLiteral}`, 'g'),
74
+ gate: /\b(?:text\/template|html\/template|template\.New)\b/ },
75
+ ];
76
+ }
77
+ return [];
78
+ }
79
+
80
+ function _lang(fp) {
81
+ if (/\.java$/i.test(fp)) return 'java';
82
+ if (/\.kt$/i.test(fp)) return 'kt';
83
+ if (/\.cs$/i.test(fp)) return 'cs';
84
+ if (/\.go$/i.test(fp)) return 'go';
85
+ return null;
86
+ }
87
+
88
+ export function scanCodeInjectionMultilang(fp, raw) {
89
+ if (!raw || raw.length > 500_000) return [];
90
+ const lang = _lang(fp);
91
+ if (!lang) return [];
92
+ const code = blankComments(raw);
93
+ const pats = patternsFor(lang);
94
+ const findings = [];
95
+ const seen = new Set();
96
+
97
+ for (const { key, re, gate } of pats) {
98
+ if (gate && !gate.test(code)) continue;
99
+ const r = new RegExp(re.source, re.flags);
100
+ let m;
101
+ while ((m = r.exec(code))) {
102
+ const line = lineOf(code, m.index);
103
+ const id = `code-injection-ml:${fp}:${line}:${key}`;
104
+ if (seen.has(id)) continue;
105
+ seen.add(id);
106
+ findings.push({
107
+ id, file: fp, line,
108
+ vuln: 'Code Injection — dynamic code/expression evaluation of a non-literal argument',
109
+ severity: 'critical', cwe: 'CWE-94', family: 'code-injection',
110
+ parser: lang.toUpperCase(), confidence: 0.7, stride: 'Elevation of Privilege',
111
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
112
+ remediation: REMEDIATION,
113
+ });
114
+ }
115
+ }
116
+ return findings;
117
+ }
@@ -54,7 +54,7 @@ function _shape(file, line, ruleId, vuln, fam, sev, cwe, remediation, descriptio
54
54
  };
55
55
  }
56
56
 
57
- const _RELEVANCE = /\bTLS\b|\bSSL\b|\btls\b|tls_version|tls_minimum|min[_-]?version|min[_-]?protocol|verify|ciph(?:er|ersuite)|NODE_TLS|InsecureSkipVerify|rejectUnauthor|trust[_-]?all|allow[_-]?all|jwt\.|jsonwebtoken|jose|PyJWT|jwt\b|MD5|SHA1|sha-?1\b|md5\b|DES\b|3DES|RC4|Blowfish|ECB|pbkdf2|PBKDF2|bcrypt|scrypt|argon2|Math\.random|random\.random|java\.util\.Random|new Random/i;
57
+ const _RELEVANCE = /\bTLS\b|\bSSL\b|\btls\b|tls_version|tls_minimum|min[_-]?version|min[_-]?protocol|verify|ciph(?:er|ersuite)|NODE_TLS|InsecureSkipVerify|rejectUnauthor|trust[_-]?all|allow[_-]?all|jwt\.|jsonwebtoken|jose|PyJWT|jwt\b|MD5|SHA1|sha-?1\b|md5\b|DES\b|3DES|RC4|Blowfish|ECB|pbkdf2|PBKDF2|bcrypt|scrypt|argon2|Math\.random|random\.random|java\.util\.Random|new Random|IvParameterSpec|GCMParameterSpec|openssl_encrypt|createCipher|\bAES\b|\baes\b|\.iv\s*=|\bIV\b/i;
58
58
 
59
59
  function _isCryptoRelevant(text) { return _RELEVANCE.test(text); }
60
60
 
@@ -148,6 +148,15 @@ function detectWeakCiphers(file, raw, code, out, seen) {
148
148
  { re: /\bciphers\s*:\s*['"`][^'"`]*(?:NULL|RC4|EXPORT|aNULL|eNULL|3DES|DES-CBC)/g },
149
149
  // Go cipher.NewTripleDESCipher
150
150
  { re: /\bcipher\.New(?:TripleDESCipher|DESCipher)\b/g },
151
+ // Go crypto/des + crypto/rc4 package constructors
152
+ { re: /\b(?:des\.New(?:TripleDESCipher|Cipher)|rc4\.NewCipher)\s*\(/g },
153
+ // PHP openssl_encrypt/openssl_decrypt with a weak algorithm string.
154
+ { re: /\bopenssl_(?:encrypt|decrypt)\s*\([^;)]*,\s*["'](?:des|des-ede3?|3des|rc4|rc2|bf|blowfish|cast5|seed)\b[^"']*["']/gi },
155
+ // PHP legacy mcrypt with DES/3DES/RC4/Blowfish.
156
+ { re: /\bmcrypt_(?:encrypt|decrypt|module_open)\s*\(\s*(?:MCRYPT_(?:DES|3DES|TRIPLEDES|ARCFOUR|RC2|RC4|BLOWFISH|CAST_128)|["'](?:des|tripledes|arcfour|rc2|rc4|blowfish)["'])/gi },
157
+ // Ruby OpenSSL::Cipher.new("DES-…"/"RC4"/"bf-…") or OpenSSL::Cipher::DES.new
158
+ { re: /\bOpenSSL::Cipher\.new\s*\(\s*["'](?:des|des-ede3?|3des|rc4|rc2|bf|blowfish|cast5|seed)\b[^"']*["']/gi },
159
+ { re: /\bOpenSSL::Cipher::(?:DES|RC4|RC2|Blowfish|CAST5)\b/g },
151
160
  ];
152
161
  for (const p of patterns) {
153
162
  let m;
@@ -196,8 +205,41 @@ function detectStaticIv(file, raw, code, out, seen) {
196
205
  { re: /\bcreateCipheriv\s*\([^,]+,\s*[^,]+,\s*['"`](?:0+|\\0+)['"`]/g },
197
206
  { re: /\bAES\.new\s*\([^,]+,[^,]+,\s*IV\s*=\s*b?['"](?:\\x00){8,}/g },
198
207
  { re: /\bcipher\.NewCBCEncrypter\s*\([^,]+,\s*make\s*\(\s*\[\]byte\s*,/g }, // make([]byte, blocksize)
208
+ // pyca/cryptography: a zero / repeated-byte literal IV — `iv = b'\x00' * 16`
209
+ // or `iv = b'\x00\x00…'` — fed to modes.CBC/CTR/CFB/OFB.
210
+ { re: /\biv\s*=\s*b['"](?:\\x00|\\0|0)+['"]\s*\*\s*\d+/gi },
211
+ { re: /\biv\s*=\s*b['"](?:\\x00|\\0){8,}['"]/gi },
212
+ { re: /\bmodes\.(?:CBC|CTR|CFB|OFB|GCM)\s*\(\s*b['"]/g }, // modes.CBC(b'…') literal IV/nonce
213
+ // Java/Kotlin: IvParameterSpec / GCMParameterSpec built from a freshly
214
+ // zero-initialized array (`new byte[16]` / `ByteArray(16)`) — no CSPRNG.
215
+ { re: /\b(?:Iv|GCM)ParameterSpec\s*\(\s*new\s+byte\s*\[\s*\d+\s*\]/g }, // Java
216
+ { re: /\b(?:Iv|GCM)ParameterSpec\s*\(\s*ByteArray\s*\(\s*\d+\s*\)/g }, // Kotlin
217
+ // Go: an IV/nonce that is `iv := make([]byte, …)` (all-zero, never filled
218
+ // from a CSPRNG) used with a stream/block-mode constructor. The `make`
219
+ // assignment usually precedes the NewXxx call, so gate on both being
220
+ // present in the file (NewXxx hint) and flag the make line.
221
+ { re: /\b\w*iv\w*\s*:?=\s*make\s*\(\s*\[\]byte\s*,/gi,
222
+ goGate: /\bcipher\.New(?:CBCEncrypter|CBCDecrypter|CTR|CFBEncrypter|CFBDecrypter|OFB)\b/,
223
+ // Safe if the file fills the IV from a CSPRNG — rand.Read(iv) /
224
+ // io.ReadFull(rand.Reader, iv) / crypto/rand usage.
225
+ goSafe: /\brand\.Read\s*\(|\bio\.ReadFull\s*\(\s*rand\.Reader|\bcrypto\/rand\b/ },
226
+ // C#: a zero/static IV assigned to .IV or passed to CreateEncryptor/Decryptor.
227
+ { re: /\.\s*IV\s*=\s*new\s+byte\s*\[\s*\d+\s*\]/g }, // C# aes.IV = new byte[16]
228
+ { re: /\bCreateEncryptor\s*\(\s*[^,]+,\s*new\s+byte\s*\[\s*\d+\s*\]/g }, // C# CreateEncryptor(key, new byte[16])
229
+ // PHP: openssl_encrypt with an empty-string IV or a str_repeat("\0",N) IV.
230
+ { re: /\bopenssl_encrypt\s*\([^;]*,\s*(?:""|''|str_repeat\s*\(\s*["']\\?0["']\s*,)/g },
231
+ { re: /\$iv\s*=\s*str_repeat\s*\(\s*["']\\?0["']\s*,\s*\d+\s*\)/g },
232
+ // Ruby: cipher.iv = a zero / fixed literal string.
233
+ { re: /\.\s*iv\s*=\s*["']\\x?0(?:\\x?0|0)*["']\s*\*\s*\d+/gi }, // cipher.iv = "\x00" * 16
234
+ { re: /\.\s*iv\s*=\s*["'](?:\\x00){4,}["']/gi }, // cipher.iv = "\x00\x00…"
235
+ { re: /\.\s*iv\s*=\s*["'][0]{8,}["']/g }, // cipher.iv = "0000…"
199
236
  ];
200
237
  for (const p of patterns) {
238
+ // Some patterns only make sense in the presence of a companion construct
239
+ // (e.g. a Go `iv := make([]byte,…)` is only suspicious when a CBC/CTR/…
240
+ // mode constructor uses it). Skip the pattern when its gate is absent.
241
+ if (p.goGate && !p.goGate.test(code)) continue;
242
+ if (p.goSafe && p.goSafe.test(code)) continue;
201
243
  let m;
202
244
  while ((m = p.re.exec(code))) {
203
245
  const ln = _line(raw, m.index);
@@ -0,0 +1,65 @@
1
+ // C# structural detectors — PRD Tier 1.
2
+ //
3
+ // The main csharp.js is flow/taint-based and misses two corpus shapes:
4
+ // - a hardcoded secret in a const/static field, SPLIT across concatenated
5
+ // literals ("sk_" + "live_…") specifically to evade secret regexes;
6
+ // - SSRF via WebClient/HttpClient on a non-validated URL.
7
+ // Both are regex/structural and complement the flow engine.
8
+
9
+ import { blankComments } from './_comment-strip.js';
10
+
11
+ const SECRET_PREFIX = /\b(?:sk_|sk-|AKIA|ghp_|gho_|xox[abps]-|AIza|eyJ|-----BEGIN|glpat-)/;
12
+ // A field whose name signals a credential, assigned a string (or concat of
13
+ // string literals — the splitting trick).
14
+ const SECRET_FIELD = /\b\w*(?:apikey|api_key|secret|token|password|passwd|pwd|credential|privatekey|connectionstring|accesskey)\w*\s*=\s*((?:"[^"]*"\s*\+\s*)*"[^"]*")/ig;
15
+
16
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
17
+ // Join the contents of a concatenation of string literals.
18
+ function joinLiterals(expr) {
19
+ const parts = expr.match(/"([^"]*)"/g) || [];
20
+ return parts.map(p => p.slice(1, -1)).join('');
21
+ }
22
+
23
+ export function scanCsharpStructural(fp, raw) {
24
+ if (!/\.cs$/i.test(fp)) return [];
25
+ if (!raw || raw.length > 500_000) return [];
26
+ const code = blankComments(raw);
27
+ const findings = [];
28
+ const seen = new Set();
29
+ const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
30
+
31
+ // Hardcoded secret in a credential-named field. The joined literal value must
32
+ // look like a real secret (length or known prefix) so header-name constants
33
+ // (ApiKeyHeader = "X-Api-Key") are not flagged.
34
+ let m;
35
+ const sre = new RegExp(SECRET_FIELD.source, SECRET_FIELD.flags);
36
+ while ((m = sre.exec(code))) {
37
+ const value = joinLiterals(m[1]);
38
+ if (value.length < 16 && !SECRET_PREFIX.test(value)) continue;
39
+ const line = lineOf(code, m.index);
40
+ push({
41
+ id: `csharp-hardcoded-secret:${fp}:${line}`, file: fp, line,
42
+ vuln: 'Hardcoded credential in a const/static field (C#)',
43
+ severity: 'high', cwe: 'CWE-798', family: 'secret', parser: 'CSHARP', confidence: 0.7,
44
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
45
+ remediation: 'Load the secret from the environment / a secrets manager (Environment.GetEnvironmentVariable, Azure Key Vault). Concatenating the literal to split it does not help — rotate the exposed value.',
46
+ });
47
+ }
48
+
49
+ // SSRF: WebClient/HttpClient fetch from a non-literal URL, unless a host
50
+ // allow/deny guard is present.
51
+ const SSRF_SINK = /\.(?:DownloadString|DownloadData|OpenRead|GetAsync|GetStringAsync|GetStreamAsync|GetByteArrayAsync)\s*\(\s*[A-Za-z_]\w*\s*\)/;
52
+ const SSRF_GUARD = /169\.254\.169\.254|\.Host\b|allow(?:ed)?Hosts?|IsLoopback|deny(?:list)?|block(?:list|ed)/i;
53
+ if (SSRF_SINK.test(code) && !SSRF_GUARD.test(code)) {
54
+ const line = lineOf(code, code.search(SSRF_SINK));
55
+ push({
56
+ id: `csharp-ssrf:${fp}:${line}`, file: fp, line,
57
+ vuln: 'SSRF — HTTP client fetch from a non-validated URL (C#)',
58
+ severity: 'high', cwe: 'CWE-918', family: 'ssrf', parser: 'CSHARP', confidence: 0.55,
59
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
60
+ remediation: 'Validate the URL host against an allow-list and reject RFC1918 / link-local / metadata (169.254.169.254) addresses before fetching.',
61
+ });
62
+ }
63
+
64
+ return findings;
65
+ }
@@ -531,6 +531,9 @@ function detectOpenRedirect(file, raw, ir, analysis, out, seen) {
531
531
  // URL is non-local. We still flag it because Juliet expects the
532
532
  // detection — but downgrade the severity.
533
533
  const isLocalRedirect = call.method === 'LocalRedirect';
534
+ // A LocalRedirect / Redirect guarded by Url.IsLocalUrl is the documented
535
+ // safe pattern — don't flag it (avoids FP on hardened handlers).
536
+ if ((isLocalRedirect || /\bUrl\.IsLocalUrl\b/.test(raw)) && /\bUrl\.IsLocalUrl\b/.test(raw)) continue;
534
537
  const id = `csharp-open-redirect:${file}:${call.line}`;
535
538
  if (seen.has(id)) continue;
536
539
  seen.add(id);
package/src/sast/csrf.js CHANGED
@@ -21,11 +21,28 @@ const STATE_CHANGE_RE = {
21
21
  django: /\b(?:require_POST|require_http_methods\s*\(\s*\[[^\]]*['"](POST|PUT|PATCH|DELETE)['"])/gi,
22
22
  fastapi: /@\w+\s*\.\s*(post|put|patch|delete)\s*\(/gi,
23
23
  spring: /@(PostMapping|PutMapping|PatchMapping|DeleteMapping)\b/g,
24
+ // Symfony: reading the POST body bag (`$request->request->get(...)`) is the
25
+ // state-change signal, plus the Route annotation method list.
26
+ symfony: /\$\w+\s*->\s*(?:request|getPayload\s*\(\s*\))\s*->\s*(?:get|getInt|getBoolean|getAlnum|getDigits|all|has)\s*\(|[#@]\[?\s*Route\s*\([^)]*methods\s*[:=]\s*[[{][^\]}]*['"](?:POST|PUT|PATCH|DELETE)['"]/gi,
27
+ // Go (gin/echo/fiber/chi/mux): a router POST/PUT/PATCH/DELETE registration,
28
+ // or net/http mux `.Methods("POST")`.
29
+ go: /\b\w+\s*\.\s*(?:POST|PUT|PATCH|DELETE)\s*\(\s*["'`]|\.\s*Methods\s*\(\s*["'](?:POST|PUT|PATCH|DELETE)["']/g,
30
+ // Ruby on Rails: a route declaration `post '/x'` / `patch` / `put` / `delete`
31
+ // (in routes.rb) or `resources :x` (full CRUD incl. state-changing verbs).
32
+ ruby: /^\s*(?:post|put|patch|delete)\s+['":]|\bresources\s+:/gim,
33
+ // C# ASP.NET MVC/Web API: a [HttpPost]/[HttpPut]/[HttpPatch]/[HttpDelete]
34
+ // action attribute.
35
+ csharp: /\[\s*Http(?:Post|Put|Patch|Delete)\b/g,
24
36
  };
25
37
 
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;
38
+ const CSRF_DEFENCE_RE = /\b(?:csurf|csrfProtection|csrf\(\)|express-csrf-token|lusca\.csrf|fastify-csrf|CSRFProtect|csrf_protect|CsrfViewMiddleware|CsrfFilter|@CsrfProtected|isCsrfTokenValid|IsCsrfTokenValid|csrf_token|CsrfToken|ValidateAntiForgeryToken|AutoValidateAntiforgeryToken|IAntiforgery|protect_from_forgery|verify_authenticity_token|form_authenticity_token|gorilla\/csrf|csrf\.Protect|nosurf|sameSite\s*[:=]\s*['"](?:Strict|Lax)['"]|origin\s*===|referer\s*===|Origin\s*===|Referer\s*===)/i;
27
39
 
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;
40
+ // Token-auth signals — these auth schemes are CSRF-safe by construction (the
41
+ // credential rides in a header the browser never auto-attaches). NOTE: bare
42
+ // `[Authorize]` is intentionally NOT here — ASP.NET cookie auth is the default
43
+ // and stays CSRF-vulnerable; only an explicit Bearer scheme or [ApiController]
44
+ // (which disables antiforgery + implies token auth) exempts.
45
+ 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|lexik_jwt|JWTTokenAuthenticator|headers\s*->\s*get\s*\(\s*['"]Authorization|c\.GetHeader\s*\(\s*["']Authorization|request\.headers\[["']Authorization)|\[\s*ApiController\b|AuthenticationSchemes\s*=\s*["']?Bearer/i;
29
46
 
30
47
  const TEST_FILE_RE = /(?:^|\/)(?:tests?|__tests__|specs?|test|fixtures)\//i;
31
48
 
@@ -39,9 +56,13 @@ export function scanCSRF(fp, raw) {
39
56
  if (/^(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(ext)) langSel = ['express', 'fastify'];
40
57
  else if (ext === 'py') langSel = ['flask', 'django', 'fastapi'];
41
58
  else if (ext === 'java' || ext === 'kt') langSel = ['spring'];
59
+ else if (ext === 'php' || ext === 'phtml') langSel = ['symfony'];
60
+ else if (ext === 'go') langSel = ['go'];
61
+ else if (ext === 'rb') langSel = ['ruby'];
62
+ else if (ext === 'cs') langSel = ['csharp'];
42
63
  if (!langSel) return [];
43
64
 
44
- const code = blankComments(raw, ext === 'py' ? 'py' : undefined);
65
+ const code = blankComments(raw, (ext === 'py' || ext === 'rb') ? 'py' : undefined);
45
66
  // Project-wide-ish: if the file shows CSRF defence anywhere or only handles
46
67
  // token-authenticated routes, we suppress.
47
68
  const csrfInScope = CSRF_DEFENCE_RE.test(code);
@@ -0,0 +1,84 @@
1
+ // CSV / formula injection (CWE-1236) — additive detector.
2
+ //
3
+ // When user-controlled text is written into a CSV/spreadsheet cell that begins
4
+ // with `=`, `+`, `-`, `@`, or a tab/CR, a spreadsheet app (Excel, Sheets,
5
+ // LibreOffice) interprets it as a FORMULA on open — enabling data exfiltration
6
+ // (`=IMPORTXML(...)`), command execution via DDE, or credential phishing. The
7
+ // fix is to neutralize the leading character (prefix with `'`), not HTML/SQL
8
+ // escaping.
9
+ //
10
+ // High precision by construction: we only fire when (a) a CSV/spreadsheet
11
+ // WRITE API is on the line, (b) a user-derived value is referenced on the same
12
+ // statement, and (c) no formula-neutralization helper is present nearby.
13
+
14
+ import { blankComments } from './_comment-strip.js';
15
+
16
+ const JS_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i;
17
+ const PY_RE = /\.py$/i;
18
+
19
+ // CSV / spreadsheet write APIs worth flagging (JS + Python).
20
+ const CSV_WRITE = [
21
+ /\bwriteRecords?\s*\(/, // csv-writer createObjectCsvWriter().writeRecords
22
+ /\bcsv\s*\.\s*stringify\s*\(/, // csv-stringify
23
+ /\bstringify\s*\(\s*[^)]*\bcolumns\b/, // csv-stringify with columns
24
+ /\bcsvStringify\s*\(/,
25
+ /\b(?:fastcsv|fast_csv|csvFormat)\s*\(/,
26
+ /\bXLSX\s*\.\s*utils\s*\.\s*(?:json_to_sheet|aoa_to_sheet|sheet_add)/,
27
+ /\bwriterow\s*\(|\bwriterows\s*\(|\bwriteheader\s*\(/, // python csv module
28
+ /\bcsv\s*\.\s*writer\s*\(/,
29
+ /\bDictWriter\s*\(/,
30
+ ];
31
+
32
+ // User-derived value hints on the same statement.
33
+ const TAINT_HINT =
34
+ /\b(?:req\.|request\.|params\.|query\.|body\.|_GET|_POST|_REQUEST|getParameter|\.get\(|user\.|profile\.|\.fields\b|row\.|record\.)/;
35
+
36
+ // Formula-neutralization already applied → suppress.
37
+ const NEUTRALIZED =
38
+ /escapeFormula|sanitizeCsv|sanitiseCsv|csvEscape|formulaEscape|stripFormula|['"`]\s*'\s*['"`]\s*\+|startsWith\(\s*['"`][=+\-@]|replace\(\s*\/\^\[=/;
39
+
40
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
41
+
42
+ export function scanCsvInjection(fp, raw) {
43
+ if (!raw || raw.length > 500_000) return [];
44
+ const isJs = JS_RE.test(fp), isPy = PY_RE.test(fp);
45
+ if (!isJs && !isPy) return [];
46
+ // Cheap pre-filter.
47
+ if (!/csv|writeRecords|XLSX|writerow|DictWriter|stringify/i.test(raw)) return [];
48
+
49
+ const code = blankComments(raw, isPy ? 'py' : undefined);
50
+ const lines = code.split('\n');
51
+ const findings = [];
52
+ const seen = new Set();
53
+
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const line = lines[i];
56
+ if (!CSV_WRITE.some(re => re.test(line))) continue;
57
+ if (!TAINT_HINT.test(line)) continue;
58
+ // Look at the line plus a small window for an explicit neutralizer.
59
+ const lo = Math.max(0, i - 4);
60
+ const near = lines.slice(lo, i + 2).join('\n');
61
+ if (NEUTRALIZED.test(near)) continue;
62
+ if (seen.has(i + 1)) continue;
63
+ seen.add(i + 1);
64
+ findings.push({
65
+ id: `csv-injection:${fp}:${i + 1}`,
66
+ severity: 'medium',
67
+ file: fp,
68
+ line: i + 1,
69
+ vuln: 'CSV / formula injection (untrusted data written to a spreadsheet cell)',
70
+ cwe: 'CWE-1236',
71
+ family: 'injection',
72
+ parser: 'SAST',
73
+ confidence: 0.55,
74
+ description:
75
+ 'User-controlled text is written to a CSV/spreadsheet without neutralizing leading formula ' +
76
+ 'characters (= + - @ tab). A spreadsheet app will execute it as a formula on open, enabling ' +
77
+ 'data exfiltration (e.g. =IMPORTXML), DDE command execution, or phishing.',
78
+ remediation:
79
+ "Prefix any cell value that starts with = + - @ or a tab/CR with a single quote (or strip it), " +
80
+ 'using a dedicated CSV-escaping helper. HTML/SQL escaping does not address this.',
81
+ });
82
+ }
83
+ return findings;
84
+ }