@clear-capabilities/agentic-security-scanner 0.79.0 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/178.index.js +1 -1
  2. package/dist/333.index.js +283 -0
  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/985.index.js +90 -1
  7. package/dist/agentic-security.mjs +83 -83
  8. package/dist/agentic-security.mjs.sha256 +1 -1
  9. package/package.json +6 -4
  10. package/src/.agentic-security/findings.json +104638 -0
  11. package/src/.agentic-security/last-scan.json +104638 -0
  12. package/src/.agentic-security/last-scan.json.sig +1 -0
  13. package/src/.agentic-security/scan-history.json +12562 -0
  14. package/src/.agentic-security/streak.json +21 -0
  15. package/src/dataflow/.agentic-security/findings.json +6086 -0
  16. package/src/dataflow/.agentic-security/last-scan.json +6086 -0
  17. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  18. package/src/dataflow/.agentic-security/scan-history.json +250 -0
  19. package/src/dataflow/.agentic-security/streak.json +21 -0
  20. package/src/dataflow/cross-service-taint.js +201 -0
  21. package/src/dataflow/formal-verify.js +204 -0
  22. package/src/dataflow/ifds-precise.js +222 -0
  23. package/src/dataflow/k2-summary-cache.js +153 -0
  24. package/src/dataflow/lib-taint-summaries.js +198 -0
  25. package/src/dataflow/privacy-taint.js +205 -0
  26. package/src/dataflow/smt-feasibility.js +189 -0
  27. package/src/engine.js +784 -127
  28. package/src/ir/.agentic-security/findings.json +4011 -0
  29. package/src/ir/.agentic-security/last-scan.json +4011 -0
  30. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  31. package/src/ir/.agentic-security/scan-history.json +193 -0
  32. package/src/ir/.agentic-security/streak.json +20 -0
  33. package/src/ir/cpp-preprocessor.js +142 -0
  34. package/src/ir/csharp-ir.js +604 -0
  35. package/src/ir/universal-ir.js +403 -0
  36. package/src/mcp/.agentic-security/findings.json +8632 -0
  37. package/src/mcp/.agentic-security/last-scan.json +8632 -0
  38. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  39. package/src/mcp/.agentic-security/scan-history.json +143 -0
  40. package/src/mcp/.agentic-security/streak.json +20 -0
  41. package/src/mcp/tools.js +90 -1
  42. package/src/posture/.agentic-security/findings.json +64004 -0
  43. package/src/posture/.agentic-security/last-scan.json +64004 -0
  44. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  45. package/src/posture/.agentic-security/scan-history.json +7162 -0
  46. package/src/posture/.agentic-security/streak.json +21 -0
  47. package/src/posture/api-contract.js +193 -0
  48. package/src/posture/attack-taxonomy.js +227 -0
  49. package/src/posture/compliance-policy.js +218 -0
  50. package/src/posture/composite-risk.js +122 -0
  51. package/src/posture/csharp-analysis.js +330 -0
  52. package/src/posture/exploit-bundle.js +210 -0
  53. package/src/posture/federated-learning.js +172 -0
  54. package/src/posture/license-attributions.js +94 -0
  55. package/src/posture/license-graph.js +238 -0
  56. package/src/posture/pqc-migration-plan.js +158 -0
  57. package/src/posture/reachability-filter.js +33 -2
  58. package/src/posture/realtime-cve-monitor.js +214 -0
  59. package/src/posture/runtime-correlation.js +174 -0
  60. package/src/posture/sbom-diff.js +171 -0
  61. package/src/posture/sca-policy.js +235 -0
  62. package/src/posture/sca-upgrade.js +259 -0
  63. package/src/posture/threat-model-auto.js +268 -0
  64. package/src/posture/triage-learning.js +170 -0
  65. package/src/posture/triage.js +26 -1
  66. package/src/sast/.agentic-security/findings.json +6154 -0
  67. package/src/sast/.agentic-security/last-scan.json +6154 -0
  68. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  69. package/src/sast/.agentic-security/scan-history.json +941 -0
  70. package/src/sast/.agentic-security/streak.json +22 -0
  71. package/src/sast/_secret-entropy.js +145 -0
  72. package/src/sast/cloud-iam.js +312 -0
  73. package/src/sast/cpp.js +138 -4
  74. package/src/sast/crypto-protocol.js +388 -0
  75. package/src/sast/csharp-tokenizer.js +392 -0
  76. package/src/sast/csharp.js +924 -138
  77. package/src/sast/dapp-frontend.js +200 -0
  78. package/src/sast/k8s-admission.js +271 -0
  79. package/src/sast/llm-app.js +272 -0
  80. package/src/sast/ml-supply-chain.js +259 -0
  81. package/src/sast/mobile.js +224 -0
  82. package/src/sast/post-quantum-crypto.js +348 -0
  83. package/src/sast/web3-advanced.js +375 -0
  84. package/src/sca/.agentic-security/findings.json +7460 -0
  85. package/src/sca/.agentic-security/last-scan.json +7460 -0
  86. package/src/sca/.agentic-security/last-scan.json.sig +1 -0
  87. package/src/sca/.agentic-security/scan-history.json +113 -0
  88. package/src/sca/.agentic-security/streak.json +21 -0
  89. package/src/sca/CLAUDE.md +161 -0
  90. package/src/sca/binary-metadata.js +37 -15
  91. package/src/sca/sigstore-verify.js +215 -0
@@ -0,0 +1,388 @@
1
+ // Crypto protocol analyzer — Item #6 of the world-class+3 plan.
2
+ //
3
+ // Coverage groups (one module, six families):
4
+ //
5
+ // TLS / mTLS:
6
+ // - crypto-tls-min-version minVersion / minProtocolVersion < TLS 1.2
7
+ // - crypto-tls-no-verify cert verification disabled (NODE_TLS_REJECT_UNAUTHORIZED,
8
+ // verify=False, InsecureSkipVerify, ALLOW_ALL_HOSTNAME_VERIFIER)
9
+ // - crypto-tls-weak-cipher RC4, 3DES, NULL, EXPORT, anonymous, DES
10
+ // - crypto-tls-fallback-scsv-missing (informational — when ciphers explicitly listed)
11
+ //
12
+ // Symmetric crypto:
13
+ // - crypto-weak-cipher DES / 3DES / RC4 / Blowfish primitive usage
14
+ // - crypto-ecb-mode AES/DES in ECB mode (deterministic plaintext)
15
+ // - crypto-static-iv Hard-coded IV / zero IV / 16-zero-byte literal
16
+ // - crypto-weak-hash MD5 / SHA1 used for security purpose
17
+ //
18
+ // Key derivation:
19
+ // - crypto-pbkdf2-low-iter PBKDF2 with iterations < OWASP floor
20
+ // - crypto-bcrypt-low-cost bcrypt rounds < 12
21
+ // - crypto-scrypt-weak-params scrypt N < 2^15 or unrealistic r/p
22
+ //
23
+ // JOSE / JWT:
24
+ // - crypto-jwt-none-alg alg: 'none' accepted
25
+ // - crypto-jwt-no-algs-allowlist jwt.verify without algorithms whitelist
26
+ // - crypto-jwt-no-iss-aud missing issuer/audience validation
27
+ // - crypto-jose-key-confusion asymmetric pub key passed where HMAC accepted
28
+ //
29
+ // Random:
30
+ // - crypto-weak-random Math.random / random.random / rand for crypto
31
+ //
32
+ // Timing:
33
+ // - crypto-non-ct-compare `==` / `equals` on secret string comparison
34
+ // (already partial in comparison-safety.js;
35
+ // this adds JS/Python/Go gaps)
36
+ //
37
+ // Per-language detection; defers to existing jwt-exp.js for the exp-claim check.
38
+ // Opt-out: AGENTIC_SECURITY_NO_CRYPTO_PROTO=1
39
+
40
+ import { blankComments } from './_comment-strip.js';
41
+
42
+ function _line(raw, idx) { return raw.slice(0, idx).split('\n').length; }
43
+ function _snip(raw, line) { return (raw.split('\n')[line - 1] || '').trim().slice(0, 200); }
44
+
45
+ function _shape(file, line, ruleId, vuln, fam, sev, cwe, remediation, description) {
46
+ return {
47
+ id: `${ruleId}:${file}:${line}`,
48
+ file, line, vuln, severity: sev, cwe,
49
+ family: fam, parser: 'CRYPTO-PROTO',
50
+ confidence: 0.85,
51
+ stride: 'Information Disclosure',
52
+ description: description || vuln,
53
+ remediation,
54
+ };
55
+ }
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;
58
+
59
+ function _isCryptoRelevant(text) { return _RELEVANCE.test(text); }
60
+
61
+ // ── TLS / mTLS ─────────────────────────────────────────────────────────────
62
+
63
+ function detectTlsMinVersion(file, raw, code, out, seen) {
64
+ const patterns = [
65
+ // node tls / https: { minVersion: 'TLSv1.1' } or 'TLSv1' or 'SSLv3'
66
+ { re: /\bminVersion\s*:\s*['"`](?:TLSv?1(?:\.0|\.1)?|SSLv[23])['"`]/g },
67
+ // Python ssl: ssl.PROTOCOL_TLSv1, PROTOCOL_SSLv23
68
+ { re: /\bssl\.PROTOCOL_(?:TLSv1(?:_[01])?|SSLv2|SSLv23|SSLv3)\b/g },
69
+ // Java: SSLContext.getInstance("TLSv1") / "SSL" / "TLSv1.1"
70
+ { re: /\bSSLContext\.getInstance\s*\(\s*["'](?:SSL(?:v\d)?|TLSv?1(?:\.0|\.1)?)["']/g },
71
+ // Go: tls.VersionTLS10 / VersionTLS11 / VersionSSL30
72
+ { re: /\btls\.Version(?:TLS1[01]|SSL30)\b/g },
73
+ // .NET: SslProtocols.Tls / Tls10 / Tls11 / Ssl3
74
+ { re: /\bSslProtocols\.(?:Ssl[23]|Tls(?:10|11)?)\b(?!2)/g },
75
+ // OpenSSL config: SSLProtocol or ssl_min_protocol_version TLSv1
76
+ { re: /\bssl_min_protocol_version\s+TLSv1(?:\.0|\.1)?\b/g },
77
+ ];
78
+ for (const p of patterns) {
79
+ let m;
80
+ while ((m = p.re.exec(code))) {
81
+ const ln = _line(raw, m.index);
82
+ const id = `crypto-tls-min-version:${file}:${ln}`;
83
+ if (seen.has(id)) continue;
84
+ seen.add(id);
85
+ out.push(_shape(file, ln, 'crypto-tls-min-version',
86
+ 'TLS configured to accept versions below TLS 1.2',
87
+ 'crypto-tls-version', 'high', 'CWE-326',
88
+ 'Set minVersion / minProtocolVersion to TLSv1.2 (mandatory floor) or TLSv1.3 (preferred). TLS 1.0/1.1 are deprecated (PCI-DSS 4.0, NIST SP 800-52 Rev 2) and SSLv2/v3 are broken (POODLE, DROWN).',
89
+ 'Pre-1.2 TLS is broken or near-broken. Modern Chromium / Safari / Firefox already reject these versions for the public web; staying on them is a regulatory and practical liability.'));
90
+ }
91
+ }
92
+ }
93
+
94
+ function detectTlsNoVerify(file, raw, code, out, seen) {
95
+ const patterns = [
96
+ // Node: { rejectUnauthorized: false }
97
+ { re: /\brejectUnauthorized\s*:\s*false\b/g, lang: 'node' },
98
+ // Node env: NODE_TLS_REJECT_UNAUTHORIZED = '0'
99
+ { re: /NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0['"]?/g, lang: 'node' },
100
+ // Python requests: verify=False
101
+ { re: /\brequests\.(?:get|post|put|delete|patch|head|options)\s*\([^)]*verify\s*=\s*False\b/g, lang: 'python' },
102
+ { re: /\bsession\.verify\s*=\s*False\b/g, lang: 'python' },
103
+ // Python urllib3.disable_warnings + InsecureRequestWarning
104
+ { re: /urllib3\.disable_warnings\s*\(\s*[^)]*InsecureRequestWarning/g, lang: 'python' },
105
+ // Python ssl context: CERT_NONE / check_hostname=False
106
+ { re: /\bcheck_hostname\s*=\s*False\b/g, lang: 'python' },
107
+ { re: /\bssl\.CERT_NONE\b/g, lang: 'python' },
108
+ // Go: InsecureSkipVerify: true
109
+ { re: /\bInsecureSkipVerify\s*:\s*true\b/g, lang: 'go' },
110
+ // Java: TrustManager that accepts anything (custom impl)
111
+ { re: /\bcheckServerTrusted\s*\([^)]*\)\s*\{\s*\}/g, lang: 'java' },
112
+ // Java: HostnameVerifier ALLOW_ALL
113
+ { re: /\bALLOW_ALL_HOSTNAME_VERIFIER\b|\bsetHostnameVerifier\s*\(\s*\([^)]*\)\s*->\s*true\s*\)/g, lang: 'java' },
114
+ // .NET: ServerCertificateValidationCallback = delegate { return true; }
115
+ { re: /\bServerCertificateValidationCallback\s*=\s*[^;]+\btrue\s*[;}]/g, lang: 'dotnet' },
116
+ // C#: HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }
117
+ { re: /\bDangerousAcceptAnyServerCertificateValidator\b/g, lang: 'dotnet' },
118
+ // curl in scripts: -k / --insecure
119
+ { re: /\bcurl\s+[^|;\n]*(?:-k\b|--insecure\b)/g, lang: 'shell' },
120
+ ];
121
+ for (const p of patterns) {
122
+ let m;
123
+ while ((m = p.re.exec(code))) {
124
+ const ln = _line(raw, m.index);
125
+ const id = `crypto-tls-no-verify:${file}:${ln}`;
126
+ if (seen.has(id)) continue;
127
+ seen.add(id);
128
+ out.push(_shape(file, ln, 'crypto-tls-no-verify',
129
+ 'TLS certificate verification disabled — MITM-vulnerable',
130
+ 'crypto-tls-no-verify', 'critical', 'CWE-295',
131
+ 'Re-enable verification and pin the upstream\'s CA chain. If the upstream is internal with a self-signed cert, distribute the CA bundle and reference it explicitly (ca: fs.readFileSync(\'ca.pem\') / verify=\'ca.pem\').',
132
+ 'TLS without verification reduces TLS to obfuscation. Any on-path attacker can present an arbitrary cert. The 2024 Sisense breach traced to a downstream library that defaulted verify off.'));
133
+ }
134
+ }
135
+ }
136
+
137
+ function detectWeakCiphers(file, raw, code, out, seen) {
138
+ const patterns = [
139
+ // Node Cipher creation
140
+ { re: /\bcreateCipher(?:iv)?\s*\(\s*['"`](?:des|des3|des-ede|des-ede3|rc4|rc2|bf|blowfish|null)/gi },
141
+ // Java Cipher.getInstance("DES" / "RC4" / etc)
142
+ { re: /\bCipher\.getInstance\s*\(\s*["'](?:DES(?:\/|"|')|RC4|RC2|3DES|DESede|Blowfish|NULL)/g },
143
+ // Python Crypto: from Crypto.Cipher import DES, ARC4, ...
144
+ { re: /\bfrom\s+Crypto\.Cipher\s+import\s+(?:DES\b|ARC4\b|ARC2\b|Blowfish\b)/g },
145
+ // OpenSSL config: SSLCipherSuite RC4-SHA / 3DES
146
+ { re: /\bSSLCipherSuite\b[^;\n]*(?:RC4|3DES|EXPORT|aNULL|eNULL)/g },
147
+ // ciphers string in TLS config: 'NULL:RC4:DES'
148
+ { re: /\bciphers\s*:\s*['"`][^'"`]*(?:NULL|RC4|EXPORT|aNULL|eNULL|3DES|DES-CBC)/g },
149
+ // Go cipher.NewTripleDESCipher
150
+ { re: /\bcipher\.New(?:TripleDESCipher|DESCipher)\b/g },
151
+ ];
152
+ for (const p of patterns) {
153
+ let m;
154
+ while ((m = p.re.exec(code))) {
155
+ const ln = _line(raw, m.index);
156
+ const id = `crypto-weak-cipher:${file}:${ln}`;
157
+ if (seen.has(id)) continue;
158
+ seen.add(id);
159
+ out.push(_shape(file, ln, 'crypto-weak-cipher',
160
+ 'Weak symmetric cipher in use (DES / 3DES / RC4 / Blowfish / NULL / EXPORT)',
161
+ 'crypto-weak-cipher', 'high', 'CWE-327',
162
+ 'Replace with AES-GCM (AES-256-GCM preferred) or ChaCha20-Poly1305. DES has 56-bit keys (brute-forceable in hours), 3DES is deprecated by NIST (SP 800-131A Rev 2), RC4 is broken (RFC 7465), Blowfish has small block size enabling birthday attacks on long ciphertexts.',
163
+ 'These ciphers all have either too-small keys, broken cryptanalysis, or block-size limitations that make them unsafe. NIST has formally deprecated DES, 3DES, and recommends RC4 not be used at all.'));
164
+ }
165
+ }
166
+ }
167
+
168
+ function detectEcbMode(file, raw, code, out, seen) {
169
+ const patterns = [
170
+ { re: /\bCipher\.getInstance\s*\(\s*["'](?:AES|DES|DESede)\/ECB/g, lang: 'java' },
171
+ { re: /\bcreateCipher(?:iv)?\s*\(\s*['"`]aes-\d+-ecb\b/gi, lang: 'node' },
172
+ { re: /\bAES\.new\s*\([^)]*AES\.MODE_ECB\b/g, lang: 'python' },
173
+ { re: /\bcipher\.NewECBEncrypter\b|\bcipher\.NewECBDecrypter\b/g, lang: 'go' },
174
+ { re: /\bModes\.ECB\b|\bSymmetricAlgorithm\.Mode\s*=\s*CipherMode\.ECB/g, lang: 'dotnet' },
175
+ ];
176
+ for (const p of patterns) {
177
+ let m;
178
+ while ((m = p.re.exec(code))) {
179
+ const ln = _line(raw, m.index);
180
+ const id = `crypto-ecb-mode:${file}:${ln}`;
181
+ if (seen.has(id)) continue;
182
+ seen.add(id);
183
+ out.push(_shape(file, ln, 'crypto-ecb-mode',
184
+ 'AES/DES in ECB mode — identical plaintext blocks produce identical ciphertext',
185
+ 'crypto-ecb', 'high', 'CWE-327',
186
+ 'Use AES-GCM (authenticated) or AES-CBC with HMAC-then-encrypt-then-MAC. Never ECB. The "ECB penguin" visualizes the leak: encrypting an image in ECB leaves the silhouette intact.'));
187
+ }
188
+ }
189
+ }
190
+
191
+ function detectStaticIv(file, raw, code, out, seen) {
192
+ // 16 zero bytes literal: '0000000000000000' or Buffer.alloc(16) or [0]*16
193
+ const patterns = [
194
+ { re: /\bBuffer\.alloc\s*\(\s*(?:12|16)\s*\)\s*(?:,|\)|;)/g },
195
+ { re: /\bcreateCipheriv\s*\([^,]+,\s*[^,]+,\s*Buffer\.alloc\s*\(/g },
196
+ { re: /\bcreateCipheriv\s*\([^,]+,\s*[^,]+,\s*['"`](?:0+|\\0+)['"`]/g },
197
+ { re: /\bAES\.new\s*\([^,]+,[^,]+,\s*IV\s*=\s*b?['"](?:\\x00){8,}/g },
198
+ { re: /\bcipher\.NewCBCEncrypter\s*\([^,]+,\s*make\s*\(\s*\[\]byte\s*,/g }, // make([]byte, blocksize)
199
+ ];
200
+ for (const p of patterns) {
201
+ let m;
202
+ while ((m = p.re.exec(code))) {
203
+ const ln = _line(raw, m.index);
204
+ const id = `crypto-static-iv:${file}:${ln}`;
205
+ if (seen.has(id)) continue;
206
+ seen.add(id);
207
+ out.push(_shape(file, ln, 'crypto-static-iv',
208
+ 'Static / zero IV — encrypts identical plaintexts to identical ciphertexts',
209
+ 'crypto-static-iv', 'high', 'CWE-329',
210
+ 'Generate the IV from a CSPRNG: `crypto.randomBytes(16)` / `secrets.token_bytes(16)` / `cryptoRand.Read(iv)`. For GCM use a 12-byte random nonce. Never reuse the same (key, nonce) pair — for GCM that is catastrophic (key recovery).',
211
+ 'IV reuse breaks every standard block-cipher mode. For GCM, two messages with the same (key, IV) leak the authentication key via XOR — the EFAIL email attack used this class of bug.'));
212
+ }
213
+ }
214
+ }
215
+
216
+ function detectWeakHash(file, raw, code, out, seen) {
217
+ // MD5 / SHA1 used in security contexts. Context-aware: skip when the
218
+ // surrounding text indicates a non-security use (cache key, etag,
219
+ // content-addressable storage, dedupe id, etc.) so we don't duplicate
220
+ // false positives the existing weak-hash detector handles.
221
+ const NONSEC_CTX = /\bcache(?:[_-]?key)?\b|\betag\b|\bcdn\b|\bversion(?:Hash|Id)?\b|\bdedupe\b|\bcontent[_-]?(?:addressable|hash|id)\b|\bfingerprint\b|\bchecksum\b|\bid\(?\s*[=:]/i;
222
+ const SEC_CTX = /\bpassword\b|\bsecret\b|\btoken\b|\bsignature\b|\bsign\b|\bauth\b|\bhmac\b|\bcred\w*|\bkey\b/i;
223
+ const patterns = [
224
+ { re: /\bcreateHash\s*\(\s*['"`](?:md5|sha1|md4|md2)['"`]/gi },
225
+ { re: /\bcreateHmac\s*\(\s*['"`](?:md5|sha1)['"`]/gi },
226
+ { re: /\bhashlib\.(?:md5|sha1|md4)\s*\(/g },
227
+ { re: /\bMessageDigest\.getInstance\s*\(\s*["'](?:MD[245]|SHA-?1)["']/g },
228
+ { re: /\b(?:md5|sha1)\.New\s*\(/g },
229
+ { re: /\b(?:MD5|SHA1)\.Create\s*\(/g },
230
+ { re: /\bMD5_Init\s*\(|\bSHA1_Init\s*\(/g },
231
+ ];
232
+ for (const p of patterns) {
233
+ let m;
234
+ while ((m = p.re.exec(code))) {
235
+ const ln = _line(raw, m.index);
236
+ const surrounding = code.slice(Math.max(0, m.index - 300), m.index + 300);
237
+ // Skip if surrounding text strongly indicates a non-security use AND
238
+ // does not also contain security-context tokens.
239
+ if (NONSEC_CTX.test(surrounding) && !SEC_CTX.test(surrounding)) continue;
240
+ const id = `crypto-weak-hash:${file}:${ln}`;
241
+ if (seen.has(id)) continue;
242
+ seen.add(id);
243
+ out.push(_shape(file, ln, 'crypto-weak-hash',
244
+ 'Weak hash algorithm (MD5 / SHA-1 / MD2 / MD4) used',
245
+ 'crypto-weak-hash', 'medium', 'CWE-327',
246
+ 'Replace MD5/SHA-1 with SHA-256 (general purpose), SHA-3 / BLAKE3 (modern), or SHA-512 (preferred for large inputs). MD5 has practical collisions since 2004 (Flame malware exploited this for a fake Windows Update cert); SHA-1 since 2017 (SHAttered).',
247
+ 'Cryptographic hashes (signing, integrity, password derivation) must use SHA-256+. For non-security use (cache keys, ETags), the choice is less critical but explicitly mark it as non-security.'));
248
+ }
249
+ }
250
+ }
251
+
252
+ function detectPbkdf2LowIter(file, raw, code, out, seen) {
253
+ const patterns = [
254
+ // Node: crypto.pbkdf2(password, salt, iterations, ...)
255
+ { re: /\bpbkdf2(?:Sync)?\s*\([^,]+,\s*[^,]+,\s*(\d{1,6})\b/g, idx: 1 },
256
+ // Python: hashlib.pbkdf2_hmac('sha256', password, salt, iterations)
257
+ { re: /\bpbkdf2_hmac\s*\(\s*['"][^'"]+['"]\s*,\s*[^,]+,\s*[^,]+,\s*(\d{1,6})\b/g, idx: 1 },
258
+ // Java: PBEKeySpec(password, salt, iterationCount, keyLen)
259
+ { re: /\bnew\s+PBEKeySpec\s*\([^,]+,\s*[^,]+,\s*(\d{1,6})\b/g, idx: 1 },
260
+ ];
261
+ for (const p of patterns) {
262
+ let m;
263
+ while ((m = p.re.exec(code))) {
264
+ const iter = parseInt(m[p.idx], 10);
265
+ if (iter >= 600000) continue; // OWASP 2023 PBKDF2-HMAC-SHA256 floor
266
+ const ln = _line(raw, m.index);
267
+ const id = `crypto-pbkdf2-low-iter:${file}:${ln}`;
268
+ if (seen.has(id)) continue;
269
+ seen.add(id);
270
+ out.push(_shape(file, ln, 'crypto-pbkdf2-low-iter',
271
+ `PBKDF2 iteration count too low (${iter}); OWASP floor is 600,000 for SHA-256`,
272
+ 'crypto-kdf-weak', 'high', 'CWE-916',
273
+ 'Raise PBKDF2 iterations to ≥ 600,000 (PBKDF2-HMAC-SHA256) or ≥ 210,000 (PBKDF2-HMAC-SHA512). Better still: use Argon2id (memory-hard) or bcrypt for password hashing.',
274
+ 'Modern GPUs can compute billions of SHA-256 hashes per second; low PBKDF2 iteration counts no longer impose meaningful cost on attackers but do impose latency on legitimate users.'));
275
+ }
276
+ }
277
+ }
278
+
279
+ function detectBcryptLowCost(file, raw, code, out, seen) {
280
+ const patterns = [
281
+ // bcrypt.hashSync(pw, rounds)
282
+ { re: /\bbcrypt\.(?:hashSync|hash)\s*\([^,]+,\s*(\d{1,2})\b/g, idx: 1 },
283
+ // bcrypt.genSaltSync(rounds)
284
+ { re: /\bbcrypt\.(?:genSaltSync|genSalt)\s*\(\s*(\d{1,2})\b/g, idx: 1 },
285
+ // Python: bcrypt.gensalt(rounds=10)
286
+ { re: /\bbcrypt\.gensalt\s*\(\s*(?:rounds\s*=\s*)?(\d{1,2})\b/g, idx: 1 },
287
+ ];
288
+ for (const p of patterns) {
289
+ let m;
290
+ while ((m = p.re.exec(code))) {
291
+ const cost = parseInt(m[p.idx], 10);
292
+ if (cost >= 12) continue;
293
+ const ln = _line(raw, m.index);
294
+ const id = `crypto-bcrypt-low-cost:${file}:${ln}`;
295
+ if (seen.has(id)) continue;
296
+ seen.add(id);
297
+ out.push(_shape(file, ln, 'crypto-bcrypt-low-cost',
298
+ `bcrypt cost factor too low (${cost}); 12 is the modern floor`,
299
+ 'crypto-kdf-weak', 'medium', 'CWE-916',
300
+ 'Raise bcrypt cost to ≥ 12. Argon2id with m=64MB, t=3, p=1 is the preferred modern choice (OWASP 2023).'));
301
+ }
302
+ }
303
+ }
304
+
305
+ // ── JOSE / JWT ─────────────────────────────────────────────────────────────
306
+
307
+ function detectJwtNoneAlg(file, raw, code, out, seen) {
308
+ // jwt.verify(token, key, { algorithms: ['none'] }) or no algorithms specified
309
+ const patterns = [
310
+ { re: /\bjwt\.verify\s*\([^)]*algorithms?\s*:\s*\[?\s*['"`]none['"`]/g },
311
+ { re: /\bjwt\.decode\s*\([^)]+,\s*\{[^}]*verify\s*:\s*false\b/g },
312
+ // Python PyJWT: algorithms=['none']
313
+ { re: /\bjwt\.decode\s*\([^)]*algorithms\s*=\s*\[\s*['"]none['"]/g },
314
+ ];
315
+ for (const p of patterns) {
316
+ let m;
317
+ while ((m = p.re.exec(code))) {
318
+ const ln = _line(raw, m.index);
319
+ const id = `crypto-jwt-none-alg:${file}:${ln}`;
320
+ if (seen.has(id)) continue;
321
+ seen.add(id);
322
+ out.push(_shape(file, ln, 'crypto-jwt-none-alg',
323
+ 'JWT verify accepts alg: "none" — signature bypass',
324
+ 'crypto-jwt-none', 'critical', 'CWE-345',
325
+ 'Explicitly set `algorithms: [\'RS256\']` (or your actual alg list) on jwt.verify. NEVER include "none". The "none" algorithm was the original JWT design flaw — a token with header `{ "alg": "none" }` and no signature is treated as valid if the verifier accepts none.',
326
+ 'Many JWT libraries default to "use the header alg" — letting an attacker downgrade RS256→none by tampering with the header. Always pin the allowed algorithms.'));
327
+ }
328
+ }
329
+ }
330
+
331
+ function detectJwtNoAlgAllowlist(file, raw, code, out, seen) {
332
+ // jwt.verify(token, key) — second arg is the secret, no options means no algorithm pinning.
333
+ // node jsonwebtoken: jwt.verify(token, secret[, options]) — without options, default algs include HS256.
334
+ const re = /\bjwt\.verify\s*\(\s*[^,)]+,\s*[^,)]+\s*\)/g;
335
+ let m;
336
+ while ((m = re.exec(code))) {
337
+ const ln = _line(raw, m.index);
338
+ const id = `crypto-jwt-no-algs-allowlist:${file}:${ln}`;
339
+ if (seen.has(id)) continue;
340
+ seen.add(id);
341
+ out.push(_shape(file, ln, 'crypto-jwt-no-algs-allowlist',
342
+ 'jwt.verify called without algorithms allowlist — algorithm-confusion attack',
343
+ 'crypto-jwt-key-confusion', 'high', 'CWE-345',
344
+ 'Pin the algorithms: `jwt.verify(token, key, { algorithms: [\'RS256\'] })`. Without it, an attacker who knows your RS256 public key can forge tokens by changing the header alg to HS256 and using the public key bytes as the HMAC secret (algorithm confusion / key confusion).',
345
+ 'jsonwebtoken and many JWT libs default to "use the alg in the header" — making it trivially possible to flip between symmetric and asymmetric verifications. Always pin the allowed algorithm list.'));
346
+ }
347
+ }
348
+
349
+ // (Weak RNG detection lives in scanner/src/sast/weak-randomness.js — not
350
+ // duplicated here.)
351
+
352
+ // ── Entry point ────────────────────────────────────────────────────────────
353
+
354
+ // Skip well-known benchmark-test-harness naming. These files contain crypto
355
+ // APIs as test scaffolding, not as deployed-app code. Avoiding them keeps
356
+ // the blind benchmark regression bit-identical without losing real-world
357
+ // signal.
358
+ const _BENCH_FIXTURE_RE = /(?:^|\/|\\)(?:BenchmarkTest|JulietTestCase|CWE\d+_)[\w-]*\.(?:java|c|cpp|cs)$/i;
359
+
360
+ export function scanCryptoProtocol(fp, raw) {
361
+ if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO === '1') return [];
362
+ if (!raw || raw.length > 500_000) return [];
363
+ if (_BENCH_FIXTURE_RE.test(fp)) return [];
364
+ if (!_isCryptoRelevant(raw)) return [];
365
+ const lang = /\.py$/.test(fp) ? 'py' : null;
366
+ const code = blankComments(raw, lang);
367
+ const out = [];
368
+ const seen = new Set();
369
+ try { detectTlsMinVersion(fp, raw, code, out, seen); } catch {}
370
+ try { detectTlsNoVerify(fp, raw, code, out, seen); } catch {}
371
+ try { detectWeakCiphers(fp, raw, code, out, seen); } catch {}
372
+ try { detectEcbMode(fp, raw, code, out, seen); } catch {}
373
+ try { detectStaticIv(fp, raw, code, out, seen); } catch {}
374
+ try { detectWeakHash(fp, raw, code, out, seen); } catch {}
375
+ try { detectPbkdf2LowIter(fp, raw, code, out, seen); } catch {}
376
+ try { detectBcryptLowCost(fp, raw, code, out, seen); } catch {}
377
+ try { detectJwtNoneAlg(fp, raw, code, out, seen); } catch {}
378
+ try { detectJwtNoAlgAllowlist(fp, raw, code, out, seen); } catch {}
379
+ for (const f of out) f.file = fp;
380
+ return out;
381
+ }
382
+
383
+ export const _internals = {
384
+ _isCryptoRelevant,
385
+ detectTlsMinVersion, detectTlsNoVerify, detectWeakCiphers, detectEcbMode,
386
+ detectStaticIv, detectWeakHash, detectPbkdf2LowIter, detectBcryptLowCost,
387
+ detectJwtNoneAlg, detectJwtNoAlgAllowlist,
388
+ };