@clear-capabilities/agentic-security-scanner 0.78.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 (126) hide show
  1. package/bin/.agentic-security/findings.json +16 -16
  2. package/bin/.agentic-security/last-scan.json +16 -16
  3. package/bin/.agentic-security/last-scan.json.sig +1 -1
  4. package/bin/.agentic-security/scan-history.json +51 -0
  5. package/bin/.agentic-security/streak.json +5 -5
  6. package/bin/agentic-security.js +22 -7
  7. package/dist/178.index.js +1 -1
  8. package/dist/333.index.js +283 -0
  9. package/dist/384.index.js +1 -1
  10. package/dist/476.index.js +5 -5
  11. package/dist/637.index.js +1 -1
  12. package/dist/700.index.js +138 -0
  13. package/dist/718.index.js +53 -0
  14. package/dist/838.index.js +1 -1
  15. package/dist/985.index.js +95 -1
  16. package/dist/agentic-security.mjs +83 -83
  17. package/dist/agentic-security.mjs.sha256 +1 -1
  18. package/package.json +6 -4
  19. package/src/.agentic-security/findings.json +29799 -7803
  20. package/src/.agentic-security/last-scan.json +29799 -7803
  21. package/src/.agentic-security/last-scan.json.sig +1 -1
  22. package/src/.agentic-security/scan-history.json +5119 -2611
  23. package/src/.agentic-security/streak.json +6 -6
  24. package/src/dataflow/.agentic-security/findings.json +2879 -308
  25. package/src/dataflow/.agentic-security/last-scan.json +2879 -308
  26. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
  27. package/src/dataflow/.agentic-security/scan-history.json +68 -520
  28. package/src/dataflow/.agentic-security/streak.json +6 -7
  29. package/src/dataflow/cross-service-taint.js +201 -0
  30. package/src/dataflow/engine.js +52 -8
  31. package/src/dataflow/formal-verify.js +204 -0
  32. package/src/dataflow/ifds-precise.js +222 -0
  33. package/src/dataflow/k2-summary-cache.js +153 -0
  34. package/src/dataflow/lib-taint-summaries.js +198 -0
  35. package/src/dataflow/privacy-taint.js +205 -0
  36. package/src/dataflow/smt-feasibility.js +189 -0
  37. package/src/engine.js +890 -132
  38. package/src/integrations/index.js +2 -1
  39. package/src/ir/.agentic-security/findings.json +240 -6
  40. package/src/ir/.agentic-security/last-scan.json +240 -6
  41. package/src/ir/.agentic-security/last-scan.json.sig +1 -1
  42. package/src/ir/.agentic-security/scan-history.json +16 -594
  43. package/src/ir/.agentic-security/streak.json +8 -9
  44. package/src/ir/callgraph.js +27 -7
  45. package/src/ir/cpp-preprocessor.js +142 -0
  46. package/src/ir/csharp-ir.js +604 -0
  47. package/src/ir/universal-ir.js +403 -0
  48. package/src/llm-validator/index.js +7 -5
  49. package/src/mcp/.agentic-security/findings.json +8632 -0
  50. package/src/mcp/.agentic-security/last-scan.json +8632 -0
  51. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  52. package/src/mcp/.agentic-security/scan-history.json +143 -0
  53. package/src/mcp/.agentic-security/streak.json +20 -0
  54. package/src/mcp/audit.js +5 -0
  55. package/src/mcp/tools.js +90 -1
  56. package/src/posture/.agentic-security/findings.json +16809 -4367
  57. package/src/posture/.agentic-security/last-scan.json +16809 -4367
  58. package/src/posture/.agentic-security/last-scan.json.sig +1 -1
  59. package/src/posture/.agentic-security/scan-history.json +6689 -177
  60. package/src/posture/.agentic-security/streak.json +8 -7
  61. package/src/posture/api-contract.js +193 -0
  62. package/src/posture/attack-taxonomy.js +227 -0
  63. package/src/posture/calibration-drift.js +2 -1
  64. package/src/posture/calibration.js +3 -2
  65. package/src/posture/compliance-policy.js +218 -0
  66. package/src/posture/composite-risk.js +122 -0
  67. package/src/posture/csharp-analysis.js +330 -0
  68. package/src/posture/exploit-bundle.js +210 -0
  69. package/src/posture/federated-learning.js +172 -0
  70. package/src/posture/fix-history.js +8 -2
  71. package/src/posture/license-attributions.js +94 -0
  72. package/src/posture/license-graph.js +238 -0
  73. package/src/posture/pqc-migration-plan.js +158 -0
  74. package/src/posture/profile.js +4 -5
  75. package/src/posture/reachability-filter.js +33 -2
  76. package/src/posture/realtime-cve-monitor.js +214 -0
  77. package/src/posture/rule-overrides.js +2 -3
  78. package/src/posture/rule-pack-signing.js +2 -3
  79. package/src/posture/rule-synthesis.js +5 -6
  80. package/src/posture/runtime-correlation.js +174 -0
  81. package/src/posture/sbom-diff.js +171 -0
  82. package/src/posture/sca-policy.js +235 -0
  83. package/src/posture/sca-upgrade.js +259 -0
  84. package/src/posture/security-trend.js +4 -7
  85. package/src/posture/state-dir.js +124 -0
  86. package/src/posture/streak.js +3 -0
  87. package/src/posture/suppressions.js +5 -8
  88. package/src/posture/threat-model-auto.js +268 -0
  89. package/src/posture/triage-learning.js +170 -0
  90. package/src/posture/triage.js +29 -6
  91. package/src/posture/validator-metrics.js +3 -6
  92. package/src/sast/.agentic-security/findings.json +996 -32
  93. package/src/sast/.agentic-security/last-scan.json +996 -32
  94. package/src/sast/.agentic-security/last-scan.json.sig +1 -1
  95. package/src/sast/.agentic-security/scan-history.json +565 -32
  96. package/src/sast/.agentic-security/streak.json +10 -8
  97. package/src/sast/_secret-entropy.js +145 -0
  98. package/src/sast/cloud-iam.js +312 -0
  99. package/src/sast/cpp.js +138 -4
  100. package/src/sast/crypto-protocol.js +388 -0
  101. package/src/sast/csharp-tokenizer.js +392 -0
  102. package/src/sast/csharp.js +924 -138
  103. package/src/sast/dapp-frontend.js +200 -0
  104. package/src/sast/db-taint.js +24 -0
  105. package/src/sast/k8s-admission.js +271 -0
  106. package/src/sast/llm-app.js +272 -0
  107. package/src/sast/ml-supply-chain.js +259 -0
  108. package/src/sast/mobile.js +224 -0
  109. package/src/sast/post-quantum-crypto.js +348 -0
  110. package/src/sast/rust.js +26 -0
  111. package/src/sast/web3-advanced.js +375 -0
  112. package/src/sca/.agentic-security/findings.json +6044 -171
  113. package/src/sca/.agentic-security/last-scan.json +6044 -171
  114. package/src/sca/.agentic-security/last-scan.json.sig +1 -1
  115. package/src/sca/.agentic-security/scan-history.json +83 -6
  116. package/src/sca/.agentic-security/streak.json +9 -9
  117. package/src/sca/CLAUDE.md +161 -0
  118. package/src/sca/binary-metadata.js +146 -0
  119. package/src/sca/py-package-functions.js +118 -0
  120. package/src/sca/sigstore-verify.js +215 -0
  121. package/src/sca/vendor-detect.js +53 -0
  122. package/src/report/.agentic-security/findings.json +0 -80
  123. package/src/report/.agentic-security/last-scan.json +0 -80
  124. package/src/report/.agentic-security/last-scan.json.sig +0 -1
  125. package/src/report/.agentic-security/scan-history.json +0 -35
  126. package/src/report/.agentic-security/streak.json +0 -22
package/src/sast/cpp.js CHANGED
@@ -81,6 +81,33 @@ function _isStrcpyGuarded(ctx) {
81
81
  return _SIZEOF_GUARD_RE.test(window);
82
82
  }
83
83
 
84
+ // Destination-size guard — Recommendation #6 of the SCA/SAST improvement
85
+ // plan. Read the first arg of a strcpy/strcat/sprintf call site and look
86
+ // backwards in the function for a `char dest[N];` declaration. If found,
87
+ // classify the buffer as "small" (≤ 256 bytes) or "large" (> 256) — the
88
+ // large case suggests the developer sized intentionally and we downgrade
89
+ // to medium confidence; the small case keeps the high-confidence finding.
90
+ // If no fixed-size declaration is found at all (heap-allocated, struct
91
+ // member, function parameter), we keep the original finding shape.
92
+ const _CHAR_BUFFER_DECL_RE = /\b(?:char|unsigned\s+char|signed\s+char|wchar_t|int8_t|uint8_t)\s+(\w+)\s*\[\s*(\d+|\w+)\s*\]\s*;/g;
93
+ function _classifyDestBuffer(ctx, destName) {
94
+ if (!destName) return { kind: 'unknown' };
95
+ const lines = ctx.raw.split('\n');
96
+ // Walk backwards from the current line; cap at file start.
97
+ const before = lines.slice(0, ctx.line).join('\n');
98
+ let bestSize = null;
99
+ let m;
100
+ const re = new RegExp(_CHAR_BUFFER_DECL_RE.source, 'g');
101
+ while ((m = re.exec(before))) {
102
+ if (m[1] !== destName) continue;
103
+ const sizeTxt = m[2];
104
+ if (/^\d+$/.test(sizeTxt)) bestSize = parseInt(sizeTxt, 10);
105
+ }
106
+ if (bestSize === null) return { kind: 'unknown' };
107
+ if (bestSize <= 256) return { kind: 'small-fixed', size: bestSize };
108
+ return { kind: 'large-fixed', size: bestSize };
109
+ }
110
+
84
111
  // Format-string: only fire when the variable holding the format string was
85
112
  // not assigned from a string literal earlier in the file.
86
113
  function _isPrintfVarLiteral(ctx, varName) {
@@ -99,10 +126,21 @@ const FINDINGS = [
99
126
  // variants on Windows and strlcpy on BSD/macOS.
100
127
  {
101
128
  id: 'cpp-strcpy', severity: 'high', cwe: 'CWE-120', family: 'buffer-overflow',
102
- re: /\b(strcpy|strcat|gets|stpcpy|sprintf)\s*\(/g,
129
+ // Capture the destination identifier so we can apply the destination-size
130
+ // guard (Recommendation #6) — large fixed buffers downgrade severity.
131
+ re: /\b(strcpy|strcat|gets|stpcpy|sprintf)\s*\(\s*(\w+)/g,
103
132
  vuln: 'Banned API — unbounded string copy/format (potential buffer overflow)',
104
133
  remediation: 'Replace with the bounded variant: strcpy → strlcpy / strcpy_s; strcat → strlcat / strcat_s; gets → fgets(buf, sizeof(buf), stdin); sprintf → snprintf(buf, sizeof(buf), "%s", v). The unbounded form will silently overflow on attacker-controlled input.',
105
- gate: (ctx) => !_isStrcpyGuarded(ctx),
134
+ gate: (ctx, m) => {
135
+ if (_isStrcpyGuarded(ctx)) return false;
136
+ // Destination classification — large fixed buffers are intentional
137
+ // and we demote them to a less-noisy emission. Small fixed buffers
138
+ // stay high-severity; unknown (heap / param) keeps the original behavior.
139
+ const destName = m && m[2];
140
+ ctx._destClass = _classifyDestBuffer(ctx, destName);
141
+ return true;
142
+ },
143
+ severityFor: (ctx) => ctx._destClass && ctx._destClass.kind === 'large-fixed' ? 'medium' : 'high',
106
144
  },
107
145
  {
108
146
  // printf/warn-family: format string is ARG 1.
@@ -196,8 +234,97 @@ const FINDINGS = [
196
234
  // common (bad) example pattern, not a real vulnerability.
197
235
  gate: (ctx) => _isCryptoContextRand(ctx),
198
236
  },
237
+
238
+ // ── Recommendation #6/7: C/C++ family expansion ──────────────────────────
239
+
240
+ // exec*-family with non-literal argument (CWE-78). Beyond system/popen.
241
+ {
242
+ id: 'cpp-exec-family', severity: 'critical', cwe: 'CWE-78', family: 'command-injection',
243
+ re: /\b(?:execl|execle|execlp|execlpe|execv|execve|execvp|execvpe|posix_spawn)\s*\(\s*(?!["'])\w+/g,
244
+ vuln: 'Command Injection — exec*() family with non-literal program path',
245
+ remediation: 'Pin the program path to a constant and pass arguments as a separate argv. Never pass user-controlled data as the program path itself; an attacker can substitute any binary on $PATH.',
246
+ },
247
+
248
+ // Weak crypto — OpenSSL legacy / EVP_des / MD5 / SHA1 (CWE-327).
249
+ {
250
+ id: 'cpp-weak-crypto-md', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
251
+ re: /\b(?:MD5_(?:Init|Update|Final|MD5)|MD4_|MD2_|SHA1_(?:Init|Update|Final|SHA1)|RIPEMD160_)\s*\(/g,
252
+ vuln: 'Weak Cryptography — legacy MD5/MD4/SHA1/RIPEMD160 hash primitive',
253
+ remediation: 'Use SHA-256 or SHA-3 via the OpenSSL EVP interface (`EVP_sha256()` → `EVP_DigestInit_ex` → `EVP_DigestUpdate` → `EVP_DigestFinal_ex`). For password hashing use Argon2 (libsodium) or scrypt.',
254
+ },
255
+ {
256
+ id: 'cpp-weak-crypto-des', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
257
+ re: /\b(?:DES_(?:set_key|ecb_encrypt|ncbc_encrypt|cbc_encrypt|ede3_cbc_encrypt)|RC2_|RC4_(?:set_key|encrypt|decrypt)|BF_(?:set_key|ecb_encrypt))\s*\(/g,
258
+ vuln: 'Weak Cryptography — legacy DES/3DES/RC2/RC4/Blowfish primitive',
259
+ remediation: 'Use AES-256 in GCM mode via the EVP interface (`EVP_aes_256_gcm()` → `EVP_EncryptInit_ex`). DES and RC4 are broken; 3DES is deprecated; Blowfish has a 64-bit block (Sweet32).',
260
+ },
261
+ {
262
+ id: 'cpp-weak-crypto-evp', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
263
+ re: /\bEVP_(?:des_(?:ede3?)?_(?:cbc|ecb|cfb|ofb)|md5|md4|md2|sha1|rc4|rc2_(?:cbc|ecb|cfb|ofb)|bf_(?:cbc|ecb|cfb|ofb))\s*\(/g,
264
+ vuln: 'Weak Cryptography — EVP factory for legacy primitive (MD5/SHA1/DES/3DES/RC2/RC4/BF)',
265
+ remediation: 'Use a modern EVP factory: `EVP_aes_256_gcm()` for AEAD, `EVP_sha256()` / `EVP_sha3_256()` for hashing.',
266
+ },
267
+
268
+ // Hardcoded secret in C/C++ (CWE-798) — a string literal assigned to a
269
+ // variable matching credential naming. Same idea as the Java/JS detector
270
+ // but tuned to C idioms.
271
+ {
272
+ id: 'cpp-hardcoded-secret', severity: 'high', cwe: 'CWE-798', family: 'hardcoded-secret',
273
+ // `static const char *password = "literal";` or `#define PASSWORD "literal"`
274
+ re: /\b(?:const\s+)?char\s*(?:\*\s*)?(?:const\s+)?(\w*(?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|cred(?:ential)?s?|priv(?:ate)?[_-]?key)\w*)\s*\[?\s*\]?\s*=\s*"([^"]{8,})"/gi,
275
+ vuln: 'Hardcoded Secret — credential-named char assigned a string literal',
276
+ remediation: 'Load secrets at runtime from environment variables (`getenv("API_KEY")`), a secrets file with restricted permissions, or a vault SDK. Never compile a literal credential into the binary — `strings(1)` extracts every literal string and the binary ships with the secret embedded.',
277
+ gate: (ctx, m) => {
278
+ // Apply the same entropy filter to avoid the 468 FPs/1 TP problem.
279
+ const val = m && m[2];
280
+ if (!val) return false;
281
+ try {
282
+ // Lazy import — avoids a circular dep on the entropy module being
283
+ // present in older snapshots.
284
+ // eslint-disable-next-line no-unused-vars
285
+ const { classifySecretCandidate } = _entropyMod || {};
286
+ if (classifySecretCandidate) {
287
+ const r = classifySecretCandidate(val);
288
+ if (r.skip) return false;
289
+ }
290
+ } catch { /* fail open */ }
291
+ return true;
292
+ },
293
+ },
294
+
295
+ // Use-after-free / double-free — CWE-416 / CWE-415. Heuristic: same pointer
296
+ // referenced in a free() call earlier in the function, then dereferenced
297
+ // (or free'd again) later. Conservative on declared `nullptr`-after-free.
298
+ {
299
+ id: 'cpp-uaf-heuristic', severity: 'high', cwe: 'CWE-416', family: 'mem-unsafe',
300
+ re: /\bfree\s*\(\s*(\w+)\s*\)\s*;[\s\S]{0,400}?\b\1\s*(?:->|\[|=\s*[^=])/g,
301
+ vuln: 'Use-After-Free heuristic — free(p) followed by p->/p[ access in same function window',
302
+ remediation: 'After `free(p)`, set `p = NULL;` immediately. The compiler can\'t catch UAF in general; explicit nulling means later derefs crash on a null check instead of executing on freed memory.',
303
+ },
304
+ {
305
+ id: 'cpp-double-free', severity: 'high', cwe: 'CWE-415', family: 'mem-unsafe',
306
+ re: /\bfree\s*\(\s*(\w+)\s*\)\s*;[\s\S]{0,400}?\bfree\s*\(\s*\1\s*\)/g,
307
+ vuln: 'Double-free — free(p) followed by free(p) without nulling in between',
308
+ remediation: 'After `free(p)`, set `p = NULL;`. `free(NULL)` is a defined no-op in C; `free(already_freed_p)` is undefined behavior that can corrupt the allocator and pivot to RCE.',
309
+ },
310
+
311
+ // Cookie / Session / token = rand() shape — when rand() is used to mint
312
+ // session identifiers, even outside a strict "crypto" context. Distinct
313
+ // from the gated rand() rule above to catch the obvious Juliet shape.
314
+ {
315
+ id: 'cpp-rand-session-token', severity: 'high', cwe: 'CWE-338', family: 'weak-rng',
316
+ re: /\b(?:session_id|token|cookie|nonce|csrf|secret_key)\s*(?:=|\.|->)[\s\S]{0,200}?\b(?:rand|random)\s*\(/gi,
317
+ vuln: 'Weak Randomness — session/token/cookie value derived from rand()/random()',
318
+ remediation: 'Use getrandom() (Linux), RAND_bytes() (OpenSSL), or BCryptGenRandom() (Windows) for any identifier that has to be unguessable. rand() outputs are predictable to within 2^31 internal states.',
319
+ },
199
320
  ];
200
321
 
322
+ // Late-bound entropy module — imported via a dynamic require shim so the
323
+ // rule table can lazy-call it from inside gate functions without creating
324
+ // a circular import at module load time.
325
+ let _entropyMod = null;
326
+ import('./_secret-entropy.js').then(m => { _entropyMod = m; }).catch(() => { _entropyMod = null; });
327
+
201
328
  function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
202
329
 
203
330
  export function scanCpp(fp, raw) {
@@ -223,15 +350,22 @@ export function scanCpp(fp, raw) {
223
350
  if (/^\s*#\s*define\b/.test(lineText)) continue;
224
351
  // Per-rule contextual gate (Action 2). Suppress when the surrounding
225
352
  // file/line context shows the call is not security-relevant.
353
+ const gateCtx = { file: fp, raw, line, lineText };
226
354
  if (typeof rule.gate === 'function') {
227
355
  try {
228
- if (!rule.gate({ file: fp, raw, line, lineText }, m)) continue;
356
+ if (!rule.gate(gateCtx, m)) continue;
229
357
  } catch { /* gate threw → fail open, keep finding */ }
230
358
  }
359
+ // Per-finding severity override (Recommendation #6 — destination-size
360
+ // classifier downgrades large-fixed-buffer strcpy to medium).
361
+ let sev = rule.severity;
362
+ if (typeof rule.severityFor === 'function') {
363
+ try { sev = rule.severityFor(gateCtx, m) || sev; } catch { /* keep default */ }
364
+ }
231
365
  out.push({
232
366
  id, file: fp, line,
233
367
  vuln: rule.vuln,
234
- severity: rule.severity,
368
+ severity: sev,
235
369
  cwe: rule.cwe,
236
370
  stride: rule.family === 'buffer-overflow' || rule.family === 'mem-unsafe' ? 'Tampering'
237
371
  : rule.family === 'command-injection' ? 'Elevation of Privilege'
@@ -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
+ };