@quantakrypto/core 0.4.1 → 0.4.3

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 (140) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +38 -10
  3. package/dist/agent-types.js.map +1 -1
  4. package/dist/baseline.js.map +1 -1
  5. package/dist/cache.js.map +1 -1
  6. package/dist/cbom.d.ts.map +1 -1
  7. package/dist/cbom.js +13 -4
  8. package/dist/cbom.js.map +1 -1
  9. package/dist/changed.js.map +1 -1
  10. package/dist/codemods/config-toggle.js.map +1 -1
  11. package/dist/codemods/registry.js.map +1 -1
  12. package/dist/comments.d.ts +26 -0
  13. package/dist/comments.d.ts.map +1 -1
  14. package/dist/comments.js +141 -1
  15. package/dist/comments.js.map +1 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/cwe.js.map +1 -1
  18. package/dist/dependencies.d.ts +3 -2
  19. package/dist/dependencies.d.ts.map +1 -1
  20. package/dist/dependencies.js +151 -4
  21. package/dist/dependencies.js.map +1 -1
  22. package/dist/detect-utils.d.ts +14 -3
  23. package/dist/detect-utils.d.ts.map +1 -1
  24. package/dist/detect-utils.js +32 -4
  25. package/dist/detect-utils.js.map +1 -1
  26. package/dist/detectors/c.d.ts.map +1 -1
  27. package/dist/detectors/c.js +190 -2
  28. package/dist/detectors/c.js.map +1 -1
  29. package/dist/detectors/csharp.d.ts.map +1 -1
  30. package/dist/detectors/csharp.js +141 -3
  31. package/dist/detectors/csharp.js.map +1 -1
  32. package/dist/detectors/go.d.ts +5 -3
  33. package/dist/detectors/go.d.ts.map +1 -1
  34. package/dist/detectors/go.js +187 -3
  35. package/dist/detectors/go.js.map +1 -1
  36. package/dist/detectors/java.d.ts.map +1 -1
  37. package/dist/detectors/java.js +168 -3
  38. package/dist/detectors/java.js.map +1 -1
  39. package/dist/detectors/pem.d.ts.map +1 -1
  40. package/dist/detectors/pem.js +49 -1
  41. package/dist/detectors/pem.js.map +1 -1
  42. package/dist/detectors/python.d.ts.map +1 -1
  43. package/dist/detectors/python.js +74 -3
  44. package/dist/detectors/python.js.map +1 -1
  45. package/dist/detectors/ruby.d.ts +9 -5
  46. package/dist/detectors/ruby.d.ts.map +1 -1
  47. package/dist/detectors/ruby.js +122 -2
  48. package/dist/detectors/ruby.js.map +1 -1
  49. package/dist/detectors/rust.d.ts +8 -3
  50. package/dist/detectors/rust.d.ts.map +1 -1
  51. package/dist/detectors/rust.js +185 -3
  52. package/dist/detectors/rust.js.map +1 -1
  53. package/dist/detectors/source.d.ts.map +1 -1
  54. package/dist/detectors/source.js +131 -13
  55. package/dist/detectors/source.js.map +1 -1
  56. package/dist/detectors/stateful-hbs.d.ts +22 -0
  57. package/dist/detectors/stateful-hbs.d.ts.map +1 -0
  58. package/dist/detectors/stateful-hbs.js +139 -0
  59. package/dist/detectors/stateful-hbs.js.map +1 -0
  60. package/dist/errors.js.map +1 -1
  61. package/dist/evidence.d.ts +48 -0
  62. package/dist/evidence.d.ts.map +1 -0
  63. package/dist/evidence.js +76 -0
  64. package/dist/evidence.js.map +1 -0
  65. package/dist/index.d.ts +4 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/inventory.d.ts +2 -0
  70. package/dist/inventory.d.ts.map +1 -1
  71. package/dist/inventory.js +27 -1
  72. package/dist/inventory.js.map +1 -1
  73. package/dist/parallel.js +15 -0
  74. package/dist/parallel.js.map +1 -1
  75. package/dist/patch-policy.js.map +1 -1
  76. package/dist/redact.d.ts.map +1 -1
  77. package/dist/redact.js +122 -7
  78. package/dist/redact.js.map +1 -1
  79. package/dist/registry.d.ts +2 -2
  80. package/dist/registry.d.ts.map +1 -1
  81. package/dist/registry.js +4 -2
  82. package/dist/registry.js.map +1 -1
  83. package/dist/remediate-pipeline.d.ts +3 -0
  84. package/dist/remediate-pipeline.d.ts.map +1 -1
  85. package/dist/remediate-pipeline.js +50 -0
  86. package/dist/remediate-pipeline.js.map +1 -1
  87. package/dist/remediate-request.js.map +1 -1
  88. package/dist/remediation.d.ts +9 -3
  89. package/dist/remediation.d.ts.map +1 -1
  90. package/dist/remediation.js +33 -6
  91. package/dist/remediation.js.map +1 -1
  92. package/dist/report.d.ts +9 -0
  93. package/dist/report.d.ts.map +1 -1
  94. package/dist/report.js +31 -0
  95. package/dist/report.js.map +1 -1
  96. package/dist/scan-worker.js.map +1 -1
  97. package/dist/scan.d.ts.map +1 -1
  98. package/dist/scan.js +16 -2
  99. package/dist/scan.js.map +1 -1
  100. package/dist/severity.js.map +1 -1
  101. package/dist/triage.js.map +1 -1
  102. package/dist/types.d.ts +8 -1
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/types.js.map +1 -1
  105. package/dist/verify.js.map +1 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/version.js.map +1 -1
  109. package/dist/walk.js.map +1 -1
  110. package/dist/worktree.d.ts.map +1 -1
  111. package/dist/worktree.js +10 -1
  112. package/dist/worktree.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/cbom.ts +13 -4
  115. package/src/comments.ts +140 -1
  116. package/src/dependencies.ts +156 -6
  117. package/src/detect-utils.ts +33 -4
  118. package/src/detectors/c.ts +202 -2
  119. package/src/detectors/csharp.ts +154 -3
  120. package/src/detectors/go.ts +209 -6
  121. package/src/detectors/java.ts +189 -3
  122. package/src/detectors/pem.ts +53 -1
  123. package/src/detectors/python.ts +82 -3
  124. package/src/detectors/ruby.ts +138 -7
  125. package/src/detectors/rust.ts +211 -7
  126. package/src/detectors/source.ts +163 -12
  127. package/src/detectors/stateful-hbs.ts +177 -0
  128. package/src/evidence.ts +124 -0
  129. package/src/index.ts +5 -1
  130. package/src/inventory.ts +31 -1
  131. package/src/parallel.ts +15 -0
  132. package/src/redact.ts +125 -8
  133. package/src/registry.ts +4 -2
  134. package/src/remediate-pipeline.ts +55 -0
  135. package/src/remediation.ts +41 -9
  136. package/src/report.ts +41 -2
  137. package/src/scan.ts +21 -2
  138. package/src/types.ts +8 -1
  139. package/src/version.ts +1 -1
  140. package/src/worktree.ts +9 -1
@@ -21,7 +21,7 @@
21
21
  */
22
22
  import type { Detector, Finding, RuleMeta } from "../types.js";
23
23
  import { JAVA_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
24
- import { CWE_BROKEN_CRYPTO } from "../cwe.js";
24
+ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
25
25
 
26
26
  /* -------------------------------------------------------------------------- */
27
27
  /* Regexes */
@@ -35,7 +35,34 @@ const RE_JAVA_GETINSTANCE =
35
35
 
36
36
  // BouncyCastle lightweight-API class instantiations.
37
37
  const RE_JAVA_BC =
38
- /\bnew\s+(RSAKeyPairGenerator|DSAKeyPairGenerator|ECKeyPairGenerator|ECDSASigner|Ed25519Signer|Ed448Signer|X25519Agreement|X448Agreement)\s*\(/g;
38
+ /\bnew\s+(RSAKeyPairGenerator|DSAKeyPairGenerator|ECKeyPairGenerator|ECDSASigner|Ed25519Signer|Ed448Signer|X25519Agreement|X448Agreement|ECDHBasicAgreement|DHBasicAgreement|X25519KeyPairGenerator|Ed25519KeyPairGenerator|RSAEngine|OAEPEncoding)\s*\(/g;
39
+
40
+ // BouncyCastle lightweight-API curve/EdDSA classes as a BARE constructor call —
41
+ // the Kotlin form (`X448KeyPairGenerator()`, no `new` keyword). RE_JAVA_BC above
42
+ // only matches the Java `new <Class>(` form, so Kotlin sources that drive the
43
+ // lightweight API directly are missed. The negative lookbehind `(?<!\bnew\s+)`
44
+ // keeps this additive: `new X448Agreement(` stays with RE_JAVA_BC (java-xdh),
45
+ // while bare `X448Agreement(` routes to the family-specific rules below. Scoped
46
+ // to the exact X448/X25519/Ed448/Ed25519 class names to stay precise.
47
+ const RE_JAVA_BC_CURVE =
48
+ /(?<!\bnew\s+)\b(X448KeyPairGenerator|X448Agreement|X448PrivateKeyParameters|X25519KeyPairGenerator|X25519Agreement|Ed448KeyPairGenerator|Ed448Signer|Ed25519KeyPairGenerator|Ed25519Signer)\s*\(/g;
49
+
50
+ // Insecure JSSE / TLS configuration expressed in Java source. Mirrors the JS
51
+ // tlsDetector in source.ts (category "tls"): a legacy SSL/TLS protocol version
52
+ // requested from SSLContext, and TLS hostname verification neutered via an
53
+ // all-trusting verifier (Apache HttpClient's NoopHostnameVerifier /
54
+ // ALLOW_ALL_HOSTNAME_VERIFIER).
55
+ const RE_JAVA_TLS_LEGACY = /\bSSLContext\s*\.\s*getInstance\s*\(\s*"(SSL|SSLv3|TLSv1)"/g;
56
+ const RE_JAVA_TLS_NOVERIFY = /\b(NoopHostnameVerifier|ALLOW_ALL_HOSTNAME_VERIFIER)\b/g;
57
+
58
+ // Identifier-form JWT/JOSE signature algorithms (audit F7). The quoted-string
59
+ // alg token ("RS256") is caught by the language-agnostic jwt-jose detector, but
60
+ // the Java JWT libraries pass the alg as an IDENTIFIER, not a string literal:
61
+ // jjwt's `SignatureAlgorithm.RS256` and auth0 java-jwt's `Algorithm.RSA256` /
62
+ // `Algorithm.ECDSA256`. ("SignatureAlgorithm" has no word boundary before its
63
+ // "Algorithm", so the auth0 branch cannot re-match the jjwt form.)
64
+ const RE_JAVA_JWT_ALG =
65
+ /\bSignatureAlgorithm\.(?:RS|PS|ES)(?:256|384|512)\b|\bAlgorithm\.(?:RSA|ECDSA)(?:256|384|512)\b/g;
39
66
 
40
67
  /* -------------------------------------------------------------------------- */
41
68
  /* Rule catalog */
@@ -138,7 +165,7 @@ const RULE_JAVA_XDH: RuleMeta = {
138
165
  title: "Java X25519/X448 key agreement",
139
166
  description: "JCA XDH / X25519 / X448 (KeyPairGenerator / KeyAgreement)",
140
167
  category: "key-exchange",
141
- severity: "low",
168
+ severity: "medium",
142
169
  confidence: "high",
143
170
  algorithm: "X25519",
144
171
  hndl: true,
@@ -158,6 +185,87 @@ const RULE_JAVA_EDDSA: RuleMeta = {
158
185
  cwe: CWE_BROKEN_CRYPTO,
159
186
  message: "Ed25519/Ed448 (Java/JCA) is a modern but still classical signature scheme.",
160
187
  };
188
+ // BouncyCastle lightweight-API rules for bare (Kotlin-form) constructor calls.
189
+ // Split by family so X448 and X25519 report their own algorithm string (java-xdh
190
+ // lumps both under "X25519"). Severity/CWE copied from the JCA siblings above.
191
+ const RULE_JAVA_BC_X448: RuleMeta = {
192
+ id: "java-bc-x448",
193
+ title: "Java/Kotlin X448 key agreement (BouncyCastle lightweight API)",
194
+ description: "BouncyCastle X448KeyPairGenerator / X448Agreement / X448PrivateKeyParameters",
195
+ category: "key-exchange",
196
+ severity: "medium",
197
+ confidence: "high",
198
+ algorithm: "X448",
199
+ hndl: true,
200
+ cwe: CWE_BROKEN_CRYPTO,
201
+ message:
202
+ "X448 (Java/Kotlin, BouncyCastle lightweight API) is modern but still classical key agreement — harvest-now-decrypt-later.",
203
+ };
204
+ const RULE_JAVA_BC_X25519: RuleMeta = {
205
+ id: "java-bc-x25519",
206
+ title: "Java/Kotlin X25519 key agreement (BouncyCastle lightweight API)",
207
+ description: "BouncyCastle X25519KeyPairGenerator / X25519Agreement",
208
+ category: "key-exchange",
209
+ severity: "medium",
210
+ confidence: "high",
211
+ algorithm: "X25519",
212
+ hndl: true,
213
+ cwe: CWE_BROKEN_CRYPTO,
214
+ message:
215
+ "X25519 (Java/Kotlin, BouncyCastle lightweight API) is modern but still classical key agreement — harvest-now-decrypt-later.",
216
+ };
217
+ const RULE_JAVA_BC_EDDSA: RuleMeta = {
218
+ id: "java-bc-eddsa",
219
+ title: "Java/Kotlin Ed25519/Ed448 signature (BouncyCastle lightweight API)",
220
+ description:
221
+ "BouncyCastle Ed448KeyPairGenerator / Ed448Signer / Ed25519KeyPairGenerator / Ed25519Signer",
222
+ category: "signature",
223
+ severity: "low",
224
+ confidence: "high",
225
+ algorithm: "EdDSA",
226
+ hndl: false,
227
+ cwe: CWE_BROKEN_CRYPTO,
228
+ message:
229
+ "Ed25519/Ed448 (Java/Kotlin, BouncyCastle lightweight API) is a modern but still classical signature scheme.",
230
+ };
231
+ const RULE_JAVA_TLS_LEGACY: RuleMeta = {
232
+ id: "java-tls-legacy-version",
233
+ title: "Legacy SSL/TLS version requested",
234
+ description: 'SSLContext.getInstance("SSL" | "SSLv3" | "TLSv1")',
235
+ category: "tls",
236
+ severity: "medium",
237
+ confidence: "high",
238
+ hndl: false,
239
+ cwe: CWE_WEAK_STRENGTH,
240
+ message: "SSL/SSLv3/TLS 1.0 are deprecated and insecure (Java/JSSE); require TLS 1.3.",
241
+ remediation: 'Use SSLContext.getInstance("TLSv1.3") and prefer PQC-hybrid key exchange.',
242
+ };
243
+ const RULE_JAVA_TLS_NOVERIFY: RuleMeta = {
244
+ id: "java-tls-hostname-verification-disabled",
245
+ title: "TLS hostname verification disabled",
246
+ description: "NoopHostnameVerifier / ALLOW_ALL_HOSTNAME_VERIFIER",
247
+ category: "tls",
248
+ severity: "high",
249
+ confidence: "high",
250
+ hndl: false,
251
+ cwe: CWE_CERT_VALIDATION,
252
+ message: "An all-trusting hostname verifier (Java) disables TLS hostname checking (MITM risk).",
253
+ remediation: "Remove the all-trusting verifier; rely on the default hostname verifier.",
254
+ };
255
+ const RULE_JAVA_JWT_ALG: RuleMeta = {
256
+ id: "java-jwt-alg",
257
+ title: "Java identifier-form JWT/JOSE algorithm",
258
+ description: "jjwt SignatureAlgorithm.RS/PS/ES* / auth0 Algorithm.RSA*/ECDSA*",
259
+ category: "signature",
260
+ severity: "high",
261
+ confidence: "high",
262
+ algorithm: "unknown",
263
+ hndl: false,
264
+ cwe: CWE_BROKEN_CRYPTO,
265
+ message:
266
+ "A classical JWT/JOSE signature algorithm (Java, identifier form) is used, forgeable by a quantum attacker.",
267
+ remediation: "ML-DSA-65 (FIPS 204); track IETF PQC JOSE/COSE algorithms",
268
+ };
161
269
 
162
270
  /**
163
271
  * Classify a `<factory>.getInstance("<alg>")` pair into a rule, or null when the
@@ -175,6 +283,11 @@ function classifyGetInstance(factory: string, rawAlg: string): RuleMeta | null {
175
283
  if (alg.includes("ED25519") || alg.includes("ED448") || alg.includes("EDDSA"))
176
284
  return RULE_JAVA_EDDSA;
177
285
  if (alg.includes("X25519") || alg.includes("X448") || alg === "XDH") return RULE_JAVA_XDH;
286
+ // RSASSA-PSS is an RSA *signature* scheme, not encryption/KEM. The string
287
+ // contains "RSA", so it must be caught here — BEFORE the generic RSA branch
288
+ // below, which would otherwise (mis)classify it as HNDL-exposed RSA
289
+ // encryption (java-rsa / hndl:true). Signatures are hndl:false. (Fixes F8.)
290
+ if (alg.includes("PSS")) return RULE_JAVA_RSA_SIGN;
178
291
  if (alg.includes("RSA")) return isSignature ? RULE_JAVA_RSA_SIGN : RULE_JAVA_RSA;
179
292
  if (alg.includes("DSA")) return RULE_JAVA_DSA; // ECDSA already handled above
180
293
  if (alg.includes("DH") || alg.includes("DIFFIEHELLMAN")) return RULE_JAVA_DH;
@@ -191,6 +304,25 @@ const BC_CLASS_RULES: Record<string, RuleMeta> = {
191
304
  Ed448Signer: RULE_JAVA_EDDSA,
192
305
  X25519Agreement: RULE_JAVA_XDH,
193
306
  X448Agreement: RULE_JAVA_XDH,
307
+ ECDHBasicAgreement: RULE_JAVA_ECDH,
308
+ DHBasicAgreement: RULE_JAVA_DH,
309
+ X25519KeyPairGenerator: RULE_JAVA_XDH,
310
+ Ed25519KeyPairGenerator: RULE_JAVA_EDDSA,
311
+ RSAEngine: RULE_JAVA_RSA,
312
+ OAEPEncoding: RULE_JAVA_RSA,
313
+ };
314
+
315
+ /** BouncyCastle lightweight-API bare-constructor class name → family rule. */
316
+ const BC_CURVE_CLASS_RULES: Record<string, RuleMeta> = {
317
+ X448KeyPairGenerator: RULE_JAVA_BC_X448,
318
+ X448Agreement: RULE_JAVA_BC_X448,
319
+ X448PrivateKeyParameters: RULE_JAVA_BC_X448,
320
+ X25519KeyPairGenerator: RULE_JAVA_BC_X25519,
321
+ X25519Agreement: RULE_JAVA_BC_X25519,
322
+ Ed448KeyPairGenerator: RULE_JAVA_BC_EDDSA,
323
+ Ed448Signer: RULE_JAVA_BC_EDDSA,
324
+ Ed25519KeyPairGenerator: RULE_JAVA_BC_EDDSA,
325
+ Ed25519Signer: RULE_JAVA_BC_EDDSA,
194
326
  };
195
327
 
196
328
  /** Detects classical asymmetric crypto in Java / Kotlin (JCA + BouncyCastle). */
@@ -209,6 +341,12 @@ export const javaDetector: Detector = {
209
341
  RULE_JAVA_DH,
210
342
  RULE_JAVA_XDH,
211
343
  RULE_JAVA_EDDSA,
344
+ RULE_JAVA_BC_X448,
345
+ RULE_JAVA_BC_X25519,
346
+ RULE_JAVA_BC_EDDSA,
347
+ RULE_JAVA_TLS_LEGACY,
348
+ RULE_JAVA_TLS_NOVERIFY,
349
+ RULE_JAVA_JWT_ALG,
212
350
  ],
213
351
  appliesTo: (f) => hasExtension(f, JAVA_EXTENSIONS),
214
352
  detect({ file, content }): Finding[] {
@@ -230,6 +368,54 @@ export const javaDetector: Detector = {
230
368
  );
231
369
  });
232
370
 
371
+ // BouncyCastle lightweight-API classes as a bare constructor call (Kotlin
372
+ // form, no `new`). RE_JAVA_BC_CURVE's negative lookbehind excludes the
373
+ // `new <Class>(` form already handled above, so this only adds the missed
374
+ // 448/25519 Kotlin-style matches — no double-counting.
375
+ eachMatch(RE_JAVA_BC_CURVE, content, (m) => {
376
+ const rule = BC_CURVE_CLASS_RULES[m[1]];
377
+ if (!rule) return;
378
+ findings.push(
379
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
380
+ );
381
+ });
382
+
383
+ // Legacy SSL/TLS protocol version requested from SSLContext.
384
+ eachMatch(RE_JAVA_TLS_LEGACY, content, (m) => {
385
+ findings.push(
386
+ findingFromRule(RULE_JAVA_TLS_LEGACY, {
387
+ file,
388
+ content,
389
+ index: m.index,
390
+ matchLength: m[0].length,
391
+ }),
392
+ );
393
+ });
394
+
395
+ // All-trusting hostname verifier — disables TLS hostname verification.
396
+ eachMatch(RE_JAVA_TLS_NOVERIFY, content, (m) => {
397
+ findings.push(
398
+ findingFromRule(RULE_JAVA_TLS_NOVERIFY, {
399
+ file,
400
+ content,
401
+ index: m.index,
402
+ matchLength: m[0].length,
403
+ }),
404
+ );
405
+ });
406
+
407
+ // Identifier-form JWT/JOSE signature algorithms (jjwt / auth0).
408
+ eachMatch(RE_JAVA_JWT_ALG, content, (m) => {
409
+ findings.push(
410
+ findingFromRule(RULE_JAVA_JWT_ALG, {
411
+ file,
412
+ content,
413
+ index: m.index,
414
+ matchLength: m[0].length,
415
+ }),
416
+ );
417
+ });
418
+
233
419
  return findings;
234
420
  },
235
421
  };
@@ -86,7 +86,8 @@ const PEM_RULES: PemRule[] = [
86
86
  cwe: CWE_HARDCODED_KEY,
87
87
  sensitive: true,
88
88
  message: "Embedded OpenSSH private key (RSA/ECDSA/Ed25519); classical and not quantum-safe.",
89
- remediation: "Rotate the key; plan migration to PQC-capable SSH (e.g. sntrup761x25519).",
89
+ remediation:
90
+ "Rotate the key; plan migration to PQC-capable SSH (prefer the mlkem768x25519-sha256 KEX, OpenSSH 10's default since Apr 2025).",
90
91
  },
91
92
  },
92
93
  {
@@ -157,6 +158,57 @@ const PEM_RULES: PemRule[] = [
157
158
  remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
158
159
  },
159
160
  },
161
+ {
162
+ re: /-----BEGIN (?:RSA )?PUBLIC KEY-----/g,
163
+ meta: {
164
+ id: "pem-public-key",
165
+ title: "Classical public key (PEM)",
166
+ description: "SubjectPublicKeyInfo / PKCS#1 RSA public key block",
167
+ category: "certificate",
168
+ severity: "low",
169
+ confidence: "high",
170
+ algorithm: "unknown",
171
+ hndl: false,
172
+ cwe: CWE_BROKEN_CRYPTO,
173
+ message:
174
+ "Embedded classical public key (RSA/EC/DSA); its key pair is not quantum-safe — forgeable signatures or classical key exchange.",
175
+ remediation: "Re-issue with PQC keys (ML-DSA / ML-KEM) as the ecosystem adopts them.",
176
+ },
177
+ },
178
+ {
179
+ re: /-----BEGIN DH PARAMETERS-----/g,
180
+ meta: {
181
+ id: "pem-dh-parameters",
182
+ title: "Diffie-Hellman parameters (PEM)",
183
+ description: "Finite-field DH group parameters block",
184
+ category: "key-exchange",
185
+ severity: "medium",
186
+ confidence: "high",
187
+ algorithm: "DH",
188
+ hndl: true,
189
+ cwe: CWE_BROKEN_CRYPTO,
190
+ message:
191
+ "Embedded finite-field Diffie-Hellman parameters; classical DH key exchange is harvest-now-decrypt-later exposed.",
192
+ remediation: "Migrate key exchange to hybrid X25519MLKEM768 (ML-KEM-768).",
193
+ },
194
+ },
195
+ {
196
+ re: /-----BEGIN (?:NEW )?CERTIFICATE REQUEST-----/g,
197
+ meta: {
198
+ id: "pem-cert-request",
199
+ title: "Certificate signing request (PEM)",
200
+ description: "PKCS#10 certificate request block",
201
+ category: "certificate",
202
+ severity: "low",
203
+ confidence: "high",
204
+ algorithm: "unknown",
205
+ hndl: false,
206
+ cwe: CWE_BROKEN_CRYPTO,
207
+ message:
208
+ "Embedded PKCS#10 CSR; carries a classical public key and will be signed with classical crypto.",
209
+ remediation: "Re-generate with PQC keys as PQC-capable CAs mature.",
210
+ },
211
+ },
160
212
  ];
161
213
 
162
214
  /** Detects PEM key/certificate material in arbitrary files. */
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import type { Detector, Finding, RuleMeta } from "../types.js";
24
24
  import { PYTHON_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
25
- import { CWE_BROKEN_CRYPTO } from "../cwe.js";
25
+ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
26
26
 
27
27
  /* -------------------------------------------------------------------------- */
28
28
  /* Precompiled regexes (module scope) */
@@ -38,8 +38,15 @@ const RE_PY_RSA_ENCRYPT = /\bpadding\.OAEP\s*\(|\bPKCS1_OAEP\.new\s*\(/g;
38
38
  const RE_PY_EC_KEYGEN = /\bec\.generate_private_key\s*\(|\bECC\.generate\s*\(/g;
39
39
  // ECDSA signatures: cryptography `ec.ECDSA(`, paramiko ECDSAKey.
40
40
  const RE_PY_ECDSA = /\bec\.ECDSA\s*\(|\bparamiko\.ECDSAKey\b|\bECDSAKey\.generate\s*\(/g;
41
+ // ECDH key agreement: cryptography `ec.ECDH(` — the actual harvest-now event in
42
+ // `private_key.exchange(ec.ECDH(), peer)`; previously missed entirely (audit F3).
43
+ const RE_PY_ECDH = /\bec\.ECDH\s*\(/g;
41
44
  // DSA: PyCryptodome `DSA.generate(`, paramiko DSSKey.
42
45
  const RE_PY_DSA = /\bDSA\.generate\s*\(|\bparamiko\.DSSKey\b|\bDSSKey\.generate\s*\(/g;
46
+ // hazmat DSA: cryptography `dsa.generate_private_key(` — the `cryptography` DSA
47
+ // keygen path (lowercase module) that RE_PY_DSA's uppercase `DSA.generate` misses
48
+ // (audit F9-python).
49
+ const RE_PY_HAZMAT_DSA = /\bdsa\.generate_private_key\s*\(/g;
43
50
  // Finite-field Diffie-Hellman (cryptography `dh`).
44
51
  const RE_PY_DH = /\bdh\.generate_parameters\s*\(|\bdh\.DHParameterNumbers\s*\(/g;
45
52
  // Modern-but-classical curve primitives (cryptography). `.generate(` anchors so
@@ -48,6 +55,15 @@ const RE_PY_X25519 = /\bX25519PrivateKey\.generate\s*\(/g;
48
55
  const RE_PY_X448 = /\bX448PrivateKey\.generate\s*\(/g;
49
56
  const RE_PY_EDDSA = /\b(?:Ed25519|Ed448)PrivateKey\.generate\s*\(|\bparamiko\.Ed25519Key\b/g;
50
57
 
58
+ // Python TLS misconfiguration. Mirrors the JS `tlsDetector` split of source.ts:
59
+ // certificate-verification bypass (requests `verify=False`, `ssl.CERT_NONE`,
60
+ // `check_hostname=False`, `ssl._create_unverified_context(`) is the high-severity
61
+ // MITM surface (CWE-295); a pinned legacy protocol (`ssl.PROTOCOL_TLSv1`) is the
62
+ // medium-severity weak-strength surface (CWE-326).
63
+ const RE_PY_TLS_REJECT =
64
+ /\bverify\s*=\s*False\b|\bssl\.CERT_NONE\b|\bcheck_hostname\s*=\s*False\b|\bssl\._create_unverified_context\s*\(/g;
65
+ const RE_PY_TLS_LEGACY = /\bPROTOCOL_TLSv1\b/g;
66
+
51
67
  /* -------------------------------------------------------------------------- */
52
68
  /* Rule catalog */
53
69
  /* -------------------------------------------------------------------------- */
@@ -106,6 +122,19 @@ const RULE_PY_ECDSA: RuleMeta = {
106
122
  message: "Classical ECDSA signing (Python) is forgeable by a quantum attacker.",
107
123
  remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
108
124
  };
125
+ const RULE_PY_ECDH: RuleMeta = {
126
+ id: "python-ecdh",
127
+ title: "Python ECDH key agreement",
128
+ description: "cryptography ec.ECDH() exchange",
129
+ category: "key-exchange",
130
+ severity: "high",
131
+ confidence: "high",
132
+ algorithm: "ECDH",
133
+ hndl: true,
134
+ cwe: CWE_BROKEN_CRYPTO,
135
+ message:
136
+ "Elliptic-curve Diffie-Hellman key agreement (Python) is broken by Shor's algorithm (harvest-now-decrypt-later).",
137
+ };
109
138
  const RULE_PY_DSA: RuleMeta = {
110
139
  id: "python-dsa",
111
140
  title: "Python DSA key/usage",
@@ -119,6 +148,20 @@ const RULE_PY_DSA: RuleMeta = {
119
148
  message: "Classical DSA (Python) is deprecated and forgeable by a quantum attacker.",
120
149
  remediation: "Rotate off DSA and migrate to ML-DSA-65 (FIPS 204).",
121
150
  };
151
+ const RULE_PY_HAZMAT_DSA: RuleMeta = {
152
+ id: "python-hazmat-dsa",
153
+ title: "Python DSA key generation (cryptography)",
154
+ description: "cryptography dsa.generate_private_key",
155
+ category: "signature",
156
+ severity: "high",
157
+ confidence: "high",
158
+ algorithm: "DSA",
159
+ hndl: false,
160
+ cwe: CWE_BROKEN_CRYPTO,
161
+ message:
162
+ "cryptography dsa.generate_private_key (Python) creates a classical DSA key; DSA is deprecated and forgeable by a quantum attacker.",
163
+ remediation: "Rotate off DSA and migrate to ML-DSA-65 (FIPS 204).",
164
+ };
122
165
  const RULE_PY_DH: RuleMeta = {
123
166
  id: "python-dh",
124
167
  title: "Python Diffie-Hellman key exchange",
@@ -137,7 +180,7 @@ const RULE_PY_X25519: RuleMeta = {
137
180
  title: "Python X25519 key exchange",
138
181
  description: "cryptography X25519PrivateKey.generate",
139
182
  category: "key-exchange",
140
- severity: "low",
183
+ severity: "medium",
141
184
  confidence: "high",
142
185
  algorithm: "X25519",
143
186
  hndl: true,
@@ -150,7 +193,7 @@ const RULE_PY_X448: RuleMeta = {
150
193
  title: "Python X448 key exchange",
151
194
  description: "cryptography X448PrivateKey.generate",
152
195
  category: "key-exchange",
153
- severity: "low",
196
+ severity: "medium",
154
197
  confidence: "high",
155
198
  algorithm: "X448",
156
199
  hndl: true,
@@ -169,6 +212,34 @@ const RULE_PY_EDDSA: RuleMeta = {
169
212
  cwe: CWE_BROKEN_CRYPTO,
170
213
  message: "Ed25519/Ed448 (Python) is a modern but still classical signature scheme.",
171
214
  };
215
+ const RULE_PY_TLS_REJECT: RuleMeta = {
216
+ id: "python-tls-reject",
217
+ title: "Python TLS certificate verification disabled",
218
+ description:
219
+ "requests verify=False / ssl.CERT_NONE / check_hostname=False / _create_unverified_context",
220
+ category: "tls",
221
+ severity: "high",
222
+ confidence: "high",
223
+ hndl: false,
224
+ cwe: CWE_CERT_VALIDATION,
225
+ message:
226
+ "TLS certificate verification is disabled (verify=False / CERT_NONE / check_hostname=False / _create_unverified_context), which allows man-in-the-middle attacks.",
227
+ remediation:
228
+ "Enable certificate verification (verify=True, ssl.CERT_REQUIRED, check_hostname=True) and verify certificates properly.",
229
+ };
230
+ const RULE_PY_TLS_LEGACY: RuleMeta = {
231
+ id: "python-tls-legacy-version",
232
+ title: "Python legacy TLS version pinned",
233
+ description: "ssl.PROTOCOL_TLSv1 (TLS 1.0)",
234
+ category: "tls",
235
+ severity: "medium",
236
+ confidence: "high",
237
+ hndl: false,
238
+ cwe: CWE_WEAK_STRENGTH,
239
+ message: "TLS 1.0 (ssl.PROTOCOL_TLSv1) is deprecated and insecure; require TLS 1.3.",
240
+ remediation:
241
+ "Use ssl.PROTOCOL_TLS_CLIENT with minimum_version = ssl.TLSVersion.TLSv1_3 and prefer PQC-hybrid key exchange.",
242
+ };
172
243
 
173
244
  /** Detects classical asymmetric crypto in Python source. */
174
245
  export const pythonDetector: Detector = {
@@ -181,11 +252,15 @@ export const pythonDetector: Detector = {
181
252
  RULE_PY_RSA_ENCRYPT,
182
253
  RULE_PY_EC_KEYGEN,
183
254
  RULE_PY_ECDSA,
255
+ RULE_PY_ECDH,
184
256
  RULE_PY_DSA,
257
+ RULE_PY_HAZMAT_DSA,
185
258
  RULE_PY_DH,
186
259
  RULE_PY_X25519,
187
260
  RULE_PY_X448,
188
261
  RULE_PY_EDDSA,
262
+ RULE_PY_TLS_REJECT,
263
+ RULE_PY_TLS_LEGACY,
189
264
  ],
190
265
  appliesTo: (f) => hasExtension(f, PYTHON_EXTENSIONS),
191
266
  detect({ file, content }): Finding[] {
@@ -201,11 +276,15 @@ export const pythonDetector: Detector = {
201
276
  add(RE_PY_RSA_ENCRYPT, RULE_PY_RSA_ENCRYPT);
202
277
  add(RE_PY_EC_KEYGEN, RULE_PY_EC_KEYGEN);
203
278
  add(RE_PY_ECDSA, RULE_PY_ECDSA);
279
+ add(RE_PY_ECDH, RULE_PY_ECDH);
204
280
  add(RE_PY_DSA, RULE_PY_DSA);
281
+ add(RE_PY_HAZMAT_DSA, RULE_PY_HAZMAT_DSA);
205
282
  add(RE_PY_DH, RULE_PY_DH);
206
283
  add(RE_PY_X25519, RULE_PY_X25519);
207
284
  add(RE_PY_X448, RULE_PY_X448);
208
285
  add(RE_PY_EDDSA, RULE_PY_EDDSA);
286
+ add(RE_PY_TLS_REJECT, RULE_PY_TLS_REJECT);
287
+ add(RE_PY_TLS_LEGACY, RULE_PY_TLS_LEGACY);
209
288
 
210
289
  return findings;
211
290
  },
@@ -1,22 +1,40 @@
1
1
  /**
2
2
  * Source-code detector for classical asymmetric cryptography in Ruby. Ruby's
3
3
  * asymmetric crypto is almost entirely `OpenSSL::PKey::{RSA,EC,DSA,DH}`, which
4
- * makes the signals precise. Lexical, same strategy as the other packs.
4
+ * makes the signals precise. Lexical, same strategy as the other packs. Beyond
5
+ * keygen it also covers RSA public-key encryption on a loaded key, DH key
6
+ * agreement, the type-agnostic `OpenSSL::PKey.read` loader, Ed25519 key
7
+ * generation, and disabled TLS peer verification (`VERIFY_NONE`).
5
8
  *
6
9
  * HNDL: RSA (keygen/encryption) and (EC)DH key agreement are harvest-now-
7
- * decrypt-later exposed (hndl:true); DSA signatures are hndl:false but forgeable.
8
- * `OpenSSL::PKey::EC` is ambiguous (feeds ECDSA + ECDH), so it is classified
9
- * conservatively as key-exchange-capable (hndl:true), like the other EC keygen
10
- * rules.
10
+ * decrypt-later exposed (hndl:true); DSA / Ed25519 signatures are hndl:false but
11
+ * forgeable. `OpenSSL::PKey::EC` is ambiguous (feeds ECDSA + ECDH), so it is
12
+ * classified conservatively as key-exchange-capable (hndl:true), like the other
13
+ * EC keygen rules; `OpenSSL::PKey.read` loads a key of unknown type and is
14
+ * likewise treated conservatively.
11
15
  */
12
16
  import type { Detector, Finding, RuleMeta } from "../types.js";
13
17
  import { RUBY_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
14
- import { CWE_BROKEN_CRYPTO } from "../cwe.js";
18
+ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION } from "../cwe.js";
15
19
 
16
20
  const RE_RB_RSA = /\bOpenSSL::PKey::RSA\.(?:new|generate)\s*\(/g;
17
21
  const RE_RB_EC = /\bOpenSSL::PKey::EC\.(?:new|generate)\s*\(/g;
18
22
  const RE_RB_DSA = /\bOpenSSL::PKey::DSA\.(?:new|generate)\s*\(/g;
19
23
  const RE_RB_DH = /\bOpenSSL::PKey::DH\.new\s*\(/g;
24
+ // RSA public-key encryption on an already-loaded key (audit F4-ruby / F9).
25
+ const RE_RB_RSA_CRYPT = /\.public_encrypt\b|\.private_decrypt\b/g;
26
+ // Finite-field DH shared-secret agreement.
27
+ const RE_RB_DH_AGREE = /\bdh_compute_key\s*\(/g;
28
+ // Type-agnostic key loader — could be RSA/EC/DSA/DH (conservative).
29
+ const RE_RB_PKEY_READ = /\bOpenSSL::PKey\.read\s*\(/g;
30
+ // Ed25519 signing key via the generic generate_key factory.
31
+ const RE_RB_ED25519 = /\bOpenSSL::PKey\.generate_key\s*\(\s*["']ED25519["']/g;
32
+ // `ed25519` gem — Ed25519::SigningKey / VerifyKey (signature keys).
33
+ const RE_RB_ED25519_GEM = /\bEd25519::(?:SigningKey|VerifyKey)\b/g;
34
+ // `rbnacl` (libsodium) — Curve25519/X25519 key agreement: PrivateKey, Box, GroupElement.
35
+ const RE_RB_RBNACL = /\bRbNaCl::(?:PrivateKey|Box|GroupElement)\b/g;
36
+ // TLS peer verification disabled (mirrors the JS tlsDetector rejectUnauthorized rule).
37
+ const RE_RB_TLS_VERIFY_NONE = /\bOpenSSL::SSL::VERIFY_NONE\b/g;
20
38
 
21
39
  const RULE_RB_RSA: RuleMeta = {
22
40
  id: "ruby-rsa",
@@ -71,6 +89,100 @@ const RULE_RB_DH: RuleMeta = {
71
89
  message:
72
90
  "Finite-field Diffie-Hellman (Ruby/OpenSSL) is broken by Shor's algorithm (harvest-now-decrypt-later).",
73
91
  };
92
+ const RULE_RB_RSA_CRYPT: RuleMeta = {
93
+ id: "ruby-rsa-crypt",
94
+ title: "Ruby RSA public-key encryption",
95
+ description: "OpenSSL::PKey::RSA#public_encrypt / #private_decrypt",
96
+ category: "kem",
97
+ severity: "high",
98
+ confidence: "high",
99
+ algorithm: "RSA",
100
+ hndl: true,
101
+ cwe: CWE_BROKEN_CRYPTO,
102
+ message:
103
+ "RSA public-key encryption/decryption (Ruby/OpenSSL) is harvest-now-decrypt-later exposed.",
104
+ };
105
+ const RULE_RB_DH_AGREE: RuleMeta = {
106
+ id: "ruby-dh-agree",
107
+ title: "Ruby Diffie-Hellman key agreement",
108
+ description: "OpenSSL DH compute_key shared-secret agreement",
109
+ category: "key-exchange",
110
+ severity: "high",
111
+ confidence: "high",
112
+ algorithm: "DH",
113
+ hndl: true,
114
+ cwe: CWE_BROKEN_CRYPTO,
115
+ message:
116
+ "Finite-field Diffie-Hellman key agreement (Ruby/OpenSSL) is broken by Shor's algorithm (harvest-now-decrypt-later).",
117
+ };
118
+ const RULE_RB_PKEY_READ: RuleMeta = {
119
+ id: "ruby-pkey-read",
120
+ title: "Ruby PKey loaded from serialized key",
121
+ description: "OpenSSL::PKey.read (type-agnostic key loader)",
122
+ category: "key-exchange",
123
+ severity: "high",
124
+ confidence: "medium",
125
+ algorithm: "unknown",
126
+ hndl: true,
127
+ cwe: CWE_BROKEN_CRYPTO,
128
+ message:
129
+ "Loads a classical asymmetric key of unknown type (RSA/EC/DSA/DH) via OpenSSL::PKey.read. Treated conservatively as key-exchange-capable (harvest-now-decrypt-later).",
130
+ remediation:
131
+ "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
132
+ };
133
+ const RULE_RB_ED25519: RuleMeta = {
134
+ id: "ruby-ed25519",
135
+ title: "Ruby Ed25519 key generation",
136
+ description: 'OpenSSL::PKey.generate_key("ED25519")',
137
+ category: "signature",
138
+ severity: "low",
139
+ confidence: "high",
140
+ algorithm: "EdDSA",
141
+ hndl: false,
142
+ cwe: CWE_BROKEN_CRYPTO,
143
+ message:
144
+ "Generates an Ed25519 signing key (Ruby/OpenSSL) — modern but classical, and forgeable by a quantum attacker.",
145
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
146
+ };
147
+ const RULE_RB_ED25519_GEM: RuleMeta = {
148
+ id: "ruby-ed25519-gem",
149
+ title: "Ruby Ed25519 signature (ed25519 gem)",
150
+ description: "ed25519 gem Ed25519::SigningKey / Ed25519::VerifyKey",
151
+ category: "signature",
152
+ severity: "low",
153
+ confidence: "high",
154
+ algorithm: "EdDSA",
155
+ hndl: false,
156
+ cwe: CWE_BROKEN_CRYPTO,
157
+ message:
158
+ "Ed25519 signing/verification via the `ed25519` gem — modern but classical, and forgeable by a quantum attacker.",
159
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
160
+ };
161
+ const RULE_RB_RBNACL: RuleMeta = {
162
+ id: "ruby-rbnacl",
163
+ title: "Ruby X25519 key agreement (rbnacl)",
164
+ description: "rbnacl (libsodium) RbNaCl::PrivateKey / Box / GroupElement",
165
+ category: "key-exchange",
166
+ severity: "medium",
167
+ confidence: "high",
168
+ algorithm: "X25519",
169
+ hndl: true,
170
+ cwe: CWE_BROKEN_CRYPTO,
171
+ message:
172
+ "Curve25519/X25519 key agreement via the `rbnacl` gem (libsodium) — modern but classical key agreement, harvest-now-decrypt-later exposed.",
173
+ };
174
+ const RULE_RB_TLS_VERIFY_NONE: RuleMeta = {
175
+ id: "ruby-tls-verify-none",
176
+ title: "Ruby TLS certificate verification disabled",
177
+ description: "OpenSSL::SSL::VERIFY_NONE",
178
+ category: "tls",
179
+ severity: "high",
180
+ confidence: "high",
181
+ hndl: false,
182
+ cwe: CWE_CERT_VALIDATION,
183
+ message: "OpenSSL::SSL::VERIFY_NONE disables TLS peer certificate verification (MITM risk).",
184
+ remediation: "Use OpenSSL::SSL::VERIFY_PEER and verify the certificate chain.",
185
+ };
74
186
 
75
187
  /** Detects classical asymmetric crypto in Ruby (OpenSSL::PKey). */
76
188
  export const rubyDetector: Detector = {
@@ -78,7 +190,19 @@ export const rubyDetector: Detector = {
78
190
  description: "Classical asymmetric crypto in Ruby (OpenSSL::PKey::{RSA,EC,DSA,DH})",
79
191
  scope: "source",
80
192
  language: "ruby",
81
- rules: [RULE_RB_RSA, RULE_RB_EC, RULE_RB_DSA, RULE_RB_DH],
193
+ rules: [
194
+ RULE_RB_RSA,
195
+ RULE_RB_EC,
196
+ RULE_RB_DSA,
197
+ RULE_RB_DH,
198
+ RULE_RB_RSA_CRYPT,
199
+ RULE_RB_DH_AGREE,
200
+ RULE_RB_PKEY_READ,
201
+ RULE_RB_ED25519,
202
+ RULE_RB_ED25519_GEM,
203
+ RULE_RB_RBNACL,
204
+ RULE_RB_TLS_VERIFY_NONE,
205
+ ],
82
206
  appliesTo: (f) => hasExtension(f, RUBY_EXTENSIONS),
83
207
  detect({ file, content }): Finding[] {
84
208
  const findings: Finding[] = [];
@@ -92,6 +216,13 @@ export const rubyDetector: Detector = {
92
216
  add(RE_RB_EC, RULE_RB_EC);
93
217
  add(RE_RB_DSA, RULE_RB_DSA);
94
218
  add(RE_RB_DH, RULE_RB_DH);
219
+ add(RE_RB_RSA_CRYPT, RULE_RB_RSA_CRYPT);
220
+ add(RE_RB_DH_AGREE, RULE_RB_DH_AGREE);
221
+ add(RE_RB_PKEY_READ, RULE_RB_PKEY_READ);
222
+ add(RE_RB_ED25519, RULE_RB_ED25519);
223
+ add(RE_RB_ED25519_GEM, RULE_RB_ED25519_GEM);
224
+ add(RE_RB_RBNACL, RULE_RB_RBNACL);
225
+ add(RE_RB_TLS_VERIFY_NONE, RULE_RB_TLS_VERIFY_NONE);
95
226
  return findings;
96
227
  },
97
228
  };