@clear-capabilities/agentic-security-scanner 0.84.1 → 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.
- package/CHANGELOG.md +404 -0
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +84 -84
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +12 -6
- package/src/.agentic-security/dpia.md +26 -0
- package/src/.agentic-security/pqc-migration-plan.json +80 -0
- package/src/.agentic-security/pqc-migration-plan.md +32 -0
- package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/.agentic-security/threat-model.json +3702 -0
- package/src/.agentic-security/threat-model.md +118 -0
- package/src/dataflow/.agentic-security/dpia.md +26 -0
- package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/dataflow/.agentic-security/threat-model.json +161 -0
- package/src/dataflow/.agentic-security/threat-model.md +43 -0
- package/src/dataflow/CLAUDE.md +17 -5
- package/src/dataflow/catalog.js +9 -0
- package/src/dataflow/engine.js +30 -4
- package/src/dataflow/index.js +8 -0
- package/src/dataflow/proof-gate.js +95 -0
- package/src/dataflow/summaries.js +44 -5
- package/src/engine.js +199 -5
- package/src/ir/.agentic-security/dpia.md +26 -0
- package/src/ir/.agentic-security/exploit-bundles.json +77 -0
- package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +6 -0
- package/src/ir/.agentic-security/threat-model.json +143 -0
- package/src/ir/.agentic-security/threat-model.md +39 -0
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/callgraph.js +12 -1
- package/src/ir/tree-sitter-loader.js +108 -0
- package/src/posture/.agentic-security/dpia.md +26 -0
- package/src/posture/.agentic-security/pqc-migration-plan.json +65 -0
- package/src/posture/.agentic-security/pqc-migration-plan.md +30 -0
- package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +6 -0
- package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
- package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/posture/.agentic-security/threat-model.json +2038 -0
- package/src/posture/.agentic-security/threat-model.md +73 -0
- package/src/posture/auditor-walkthrough.js +1 -1
- package/src/posture/corpus-status.js +74 -0
- package/src/posture/coverage-report.js +113 -0
- package/src/posture/holdout-eval.js +57 -0
- package/src/posture/pr-augment.js +1 -1
- package/src/posture/router.js +37 -7
- package/src/report/.agentic-security/dpia.md +26 -0
- package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
- package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +6 -0
- package/src/report/.agentic-security/threat-model.json +7 -0
- package/src/report/.agentic-security/threat-model.md +22 -0
- package/src/report/index.js +18 -1
- package/src/sast/.agentic-security/dpia.md +26 -0
- package/src/sast/.agentic-security/pqc-migration-plan.json +65 -0
- package/src/sast/.agentic-security/pqc-migration-plan.md +30 -0
- package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +6 -0
- package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +6 -0
- package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +6 -0
- package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +6 -0
- package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +6 -0
- package/src/sast/.agentic-security/threat-model.json +816 -0
- package/src/sast/.agentic-security/threat-model.md +91 -0
- package/src/sast/CLAUDE.md +9 -3
- package/src/sast/code-injection-multilang.js +117 -0
- package/src/sast/crypto-protocol.js +43 -1
- package/src/sast/csharp-structural.js +65 -0
- package/src/sast/csharp.js +3 -0
- package/src/sast/csrf.js +24 -3
- package/src/sast/csv-injection.js +84 -0
- package/src/sast/frontend-hygiene.js +106 -0
- package/src/sast/go-structural.js +60 -0
- package/src/sast/java-structural.js +81 -0
- package/src/sast/js-framework-structural.js +115 -0
- package/src/sast/kotlin.js +72 -0
- package/src/sast/ldap-injection.js +73 -17
- package/src/sast/open-redirect.js +47 -5
- package/src/sast/php.js +35 -0
- package/src/sast/python-structural.js +83 -0
- package/src/sast/redos-nfa.js +75 -0
- package/src/sast/response-splitting.js +48 -19
- package/src/sast/ruby.js +38 -0
- package/src/sast/secret-concat.js +60 -0
- package/src/sast/ssrf-cloud-metadata.js +13 -0
- package/src/sast/stored-taint.js +88 -0
- package/src/sast/tree-sitter-sinks.js +62 -0
- package/src/sast/weak-password-hash.js +28 -0
- package/src/sast/weak-randomness.js +46 -5
- package/src/sast/wrong-context-sanitizer.js +94 -0
- package/src/sast/xpath-injection.js +30 -6
- package/src/sast/xss-reflected-multilang.js +116 -0
- 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
|
package/src/sast/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
-
**
|
|
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
|
+
}
|
package/src/sast/csharp.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|