@quantakrypto/core 0.3.0 → 0.4.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 (166) hide show
  1. package/dist/agent-types.d.ts +48 -0
  2. package/dist/agent-types.d.ts.map +1 -0
  3. package/dist/agent-types.js +2 -0
  4. package/dist/agent-types.js.map +1 -0
  5. package/dist/cache.d.ts +27 -0
  6. package/dist/cache.d.ts.map +1 -0
  7. package/dist/cache.js +93 -0
  8. package/dist/cache.js.map +1 -0
  9. package/dist/cbom.d.ts.map +1 -1
  10. package/dist/cbom.js +26 -1
  11. package/dist/cbom.js.map +1 -1
  12. package/dist/changed.d.ts.map +1 -1
  13. package/dist/changed.js +14 -2
  14. package/dist/changed.js.map +1 -1
  15. package/dist/codemods/config-toggle.d.ts +3 -0
  16. package/dist/codemods/config-toggle.d.ts.map +1 -0
  17. package/dist/codemods/config-toggle.js +26 -0
  18. package/dist/codemods/config-toggle.js.map +1 -0
  19. package/dist/codemods/registry.d.ts +26 -0
  20. package/dist/codemods/registry.d.ts.map +1 -0
  21. package/dist/codemods/registry.js +8 -0
  22. package/dist/codemods/registry.js.map +1 -0
  23. package/dist/comments.d.ts +45 -0
  24. package/dist/comments.d.ts.map +1 -0
  25. package/dist/comments.js +164 -0
  26. package/dist/comments.js.map +1 -0
  27. package/dist/config.d.ts +1 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +7 -0
  30. package/dist/config.js.map +1 -1
  31. package/dist/dependencies.d.ts +31 -13
  32. package/dist/dependencies.d.ts.map +1 -1
  33. package/dist/dependencies.js +460 -40
  34. package/dist/dependencies.js.map +1 -1
  35. package/dist/detect-utils.d.ts +44 -1
  36. package/dist/detect-utils.d.ts.map +1 -1
  37. package/dist/detect-utils.js +113 -17
  38. package/dist/detect-utils.js.map +1 -1
  39. package/dist/detectors/c.d.ts +20 -0
  40. package/dist/detectors/c.d.ts.map +1 -0
  41. package/dist/detectors/c.js +104 -0
  42. package/dist/detectors/c.js.map +1 -0
  43. package/dist/detectors/csharp.d.ts +15 -0
  44. package/dist/detectors/csharp.d.ts.map +1 -0
  45. package/dist/detectors/csharp.js +76 -0
  46. package/dist/detectors/csharp.js.map +1 -0
  47. package/dist/detectors/go.d.ts +25 -0
  48. package/dist/detectors/go.d.ts.map +1 -0
  49. package/dist/detectors/go.js +132 -0
  50. package/dist/detectors/go.js.map +1 -0
  51. package/dist/detectors/java.d.ts +25 -0
  52. package/dist/detectors/java.d.ts.map +1 -0
  53. package/dist/detectors/java.js +200 -0
  54. package/dist/detectors/java.js.map +1 -0
  55. package/dist/detectors/python.d.ts +26 -0
  56. package/dist/detectors/python.d.ts.map +1 -0
  57. package/dist/detectors/python.js +171 -0
  58. package/dist/detectors/python.js.map +1 -0
  59. package/dist/detectors/ruby.d.ts +15 -0
  60. package/dist/detectors/ruby.d.ts.map +1 -0
  61. package/dist/detectors/ruby.js +75 -0
  62. package/dist/detectors/ruby.js.map +1 -0
  63. package/dist/detectors/rust.d.ts +13 -0
  64. package/dist/detectors/rust.d.ts.map +1 -0
  65. package/dist/detectors/rust.js +94 -0
  66. package/dist/detectors/rust.js.map +1 -0
  67. package/dist/detectors/source.d.ts.map +1 -1
  68. package/dist/detectors/source.js +70 -22
  69. package/dist/detectors/source.js.map +1 -1
  70. package/dist/index.d.ts +18 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +15 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/parallel.d.ts +4 -0
  75. package/dist/parallel.d.ts.map +1 -1
  76. package/dist/parallel.js +59 -8
  77. package/dist/parallel.js.map +1 -1
  78. package/dist/patch-policy.d.ts +22 -0
  79. package/dist/patch-policy.d.ts.map +1 -0
  80. package/dist/patch-policy.js +21 -0
  81. package/dist/patch-policy.js.map +1 -0
  82. package/dist/redact.d.ts +19 -0
  83. package/dist/redact.d.ts.map +1 -0
  84. package/dist/redact.js +77 -0
  85. package/dist/redact.js.map +1 -0
  86. package/dist/registry.d.ts +13 -3
  87. package/dist/registry.d.ts.map +1 -1
  88. package/dist/registry.js +31 -4
  89. package/dist/registry.js.map +1 -1
  90. package/dist/remediate-pipeline.d.ts +41 -0
  91. package/dist/remediate-pipeline.d.ts.map +1 -0
  92. package/dist/remediate-pipeline.js +38 -0
  93. package/dist/remediate-pipeline.js.map +1 -0
  94. package/dist/remediate-request.d.ts +21 -0
  95. package/dist/remediate-request.d.ts.map +1 -0
  96. package/dist/remediate-request.js +28 -0
  97. package/dist/remediate-request.js.map +1 -0
  98. package/dist/remediation.d.ts +10 -0
  99. package/dist/remediation.d.ts.map +1 -1
  100. package/dist/remediation.js +21 -8
  101. package/dist/remediation.js.map +1 -1
  102. package/dist/report.d.ts.map +1 -1
  103. package/dist/report.js +16 -1
  104. package/dist/report.js.map +1 -1
  105. package/dist/scan-worker.js +6 -2
  106. package/dist/scan-worker.js.map +1 -1
  107. package/dist/scan.d.ts +5 -4
  108. package/dist/scan.d.ts.map +1 -1
  109. package/dist/scan.js +52 -29
  110. package/dist/scan.js.map +1 -1
  111. package/dist/triage.d.ts +27 -0
  112. package/dist/triage.d.ts.map +1 -0
  113. package/dist/triage.js +33 -0
  114. package/dist/triage.js.map +1 -0
  115. package/dist/types.d.ts +53 -3
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/verify.d.ts +30 -0
  118. package/dist/verify.d.ts.map +1 -0
  119. package/dist/verify.js +65 -0
  120. package/dist/verify.js.map +1 -0
  121. package/dist/version.d.ts +1 -1
  122. package/dist/version.js +1 -1
  123. package/dist/walk.d.ts +7 -0
  124. package/dist/walk.d.ts.map +1 -1
  125. package/dist/walk.js +85 -11
  126. package/dist/walk.js.map +1 -1
  127. package/dist/worktree.d.ts +6 -0
  128. package/dist/worktree.d.ts.map +1 -0
  129. package/dist/worktree.js +43 -0
  130. package/dist/worktree.js.map +1 -0
  131. package/package.json +1 -1
  132. package/src/agent-types.ts +52 -0
  133. package/src/cache.ts +120 -0
  134. package/src/cbom.ts +27 -1
  135. package/src/changed.ts +13 -4
  136. package/src/codemods/config-toggle.ts +35 -0
  137. package/src/codemods/registry.ts +31 -0
  138. package/src/comments.ts +180 -0
  139. package/src/config.ts +8 -0
  140. package/src/dependencies.ts +483 -51
  141. package/src/detect-utils.ts +123 -15
  142. package/src/detectors/c.ts +132 -0
  143. package/src/detectors/csharp.ts +98 -0
  144. package/src/detectors/go.ts +167 -0
  145. package/src/detectors/java.ts +235 -0
  146. package/src/detectors/python.ts +212 -0
  147. package/src/detectors/ruby.ts +97 -0
  148. package/src/detectors/rust.ts +112 -0
  149. package/src/detectors/source.ts +69 -21
  150. package/src/index.ts +45 -1
  151. package/src/parallel.ts +63 -6
  152. package/src/patch-policy.ts +42 -0
  153. package/src/redact.ts +92 -0
  154. package/src/registry.ts +32 -4
  155. package/src/remediate-pipeline.ts +81 -0
  156. package/src/remediate-request.ts +52 -0
  157. package/src/remediation.ts +27 -11
  158. package/src/report.ts +27 -1
  159. package/src/scan-worker.ts +6 -2
  160. package/src/scan.ts +62 -31
  161. package/src/triage.ts +58 -0
  162. package/src/types.ts +66 -3
  163. package/src/verify.ts +82 -0
  164. package/src/version.ts +1 -1
  165. package/src/walk.ts +81 -12
  166. package/src/worktree.ts +45 -0
@@ -1,17 +1,56 @@
1
1
  /**
2
- * Curated database of npm packages that primarily expose classical asymmetric
3
- * cryptography (and are therefore quantum-vulnerable), plus a manifest scanner
4
- * that flags any of them found in package.json / package-lock.json.
2
+ * Curated database of packages that primarily expose classical asymmetric
3
+ * cryptography (and are therefore quantum-vulnerable), across multiple
4
+ * ecosystems (npm, PyPI, crates.io, Go modules, Maven, RubyGems), plus a
5
+ * manifest scanner that flags any of them found in the corresponding manifest
6
+ * (package.json, requirements.txt, Cargo.toml, go.mod, pom.xml, Gemfile, …).
5
7
  *
6
8
  * The list is intentionally focused on libraries whose *purpose* is classical
7
- * public-key crypto. General-purpose packages that merely call out to Node's
8
- * crypto are out of scope here (those are caught by the source detectors).
9
+ * public-key crypto. General-purpose packages that merely call the language's
10
+ * stdlib crypto are out of scope here (those are caught by the source detectors).
11
+ * Extraction is generous but every finding must match a curated entry, so a new
12
+ * manifest format is cheap to add (see `manifestEcosystem` + `candidateNames`).
9
13
  */
10
- import type { AlgorithmFamily, Finding, VulnerableDependency } from "./types.js";
14
+ import type {
15
+ AlgorithmFamily,
16
+ DependencyEcosystem,
17
+ Finding,
18
+ RuleMeta,
19
+ VulnerableDependency,
20
+ } from "./types.js";
11
21
  import { makeFinding } from "./detect-utils.js";
12
- import { remediationText } from "./remediation.js";
22
+ import { isConfidentialityFamily, isSignatureFamily, remediationText } from "./remediation.js";
13
23
  import { CWE_BROKEN_CRYPTO } from "./cwe.js";
14
24
 
25
+ /**
26
+ * Catalog entry for the `dep-vulnerable` rule. Dependency findings are produced
27
+ * by the manifest scanner rather than a registered {@link Detector}, so this
28
+ * rule is absent from `defaultRegistry.ruleCatalog()`. Reporters (SARIF) merge
29
+ * it in so the rule advertises a GENERIC description — the per-package specifics
30
+ * (title/severity/HNDL/remediation) live on each individual finding/result, not
31
+ * on the shared rule. Without this, SARIF would leak the first dependency's
32
+ * package-specific text into the rule-level `shortDescription` for every dep.
33
+ */
34
+ export const DEP_VULNERABLE_RULE: RuleMeta = {
35
+ id: "dep-vulnerable",
36
+ title: "Quantum-vulnerable dependency",
37
+ category: "dependency",
38
+ // Representative default; each SARIF result carries its own per-package level.
39
+ severity: "high",
40
+ confidence: "high",
41
+ hndl: false,
42
+ cwe: CWE_BROKEN_CRYPTO,
43
+ message:
44
+ "A dependency implements classical public-key cryptography that is broken by " +
45
+ "quantum computers. Replace or upgrade it to a post-quantum alternative.",
46
+ remediation:
47
+ "Move to a post-quantum alternative — ML-KEM-768 (FIPS 203) for key exchange/" +
48
+ "encryption and ML-DSA-65 (FIPS 204) for signatures, ideally via a hybrid.",
49
+ description:
50
+ "Flags packages in a dependency manifest known to implement quantum-vulnerable " +
51
+ "public-key cryptography (RSA, ECDH/ECDSA, DH, EdDSA, …).",
52
+ };
53
+
15
54
  /** Known quantum-vulnerable npm dependencies. */
16
55
  export const vulnerableDependencies: VulnerableDependency[] = [
17
56
  {
@@ -259,50 +298,367 @@ export const vulnerableDependencies: VulnerableDependency[] = [
259
298
  algorithms: ["ECDSA"],
260
299
  severity: "low",
261
300
  },
301
+
302
+ // --- PyPI (Python) ---
303
+ {
304
+ name: "pycryptodome",
305
+ ecosystem: "pypi",
306
+ reason: "RSA / ECC / DSA public-key crypto for Python.",
307
+ algorithms: ["RSA", "ECDSA", "DSA"],
308
+ severity: "high",
309
+ },
310
+ {
311
+ name: "pycryptodomex",
312
+ ecosystem: "pypi",
313
+ reason: "RSA / ECC / DSA public-key crypto (the `Cryptodome` namespace).",
314
+ algorithms: ["RSA", "ECDSA", "DSA"],
315
+ severity: "high",
316
+ },
317
+ {
318
+ name: "rsa",
319
+ ecosystem: "pypi",
320
+ reason: "Pure-Python RSA encryption and signing.",
321
+ algorithms: ["RSA"],
322
+ severity: "high",
323
+ },
324
+ {
325
+ name: "ecdsa",
326
+ ecosystem: "pypi",
327
+ reason: "Pure-Python ECDSA/ECDH over NIST + secp256k1 curves.",
328
+ algorithms: ["ECDSA", "ECDH"],
329
+ severity: "high",
330
+ },
331
+ {
332
+ name: "cryptography",
333
+ ecosystem: "pypi",
334
+ reason: "General crypto library exposing classical RSA/EC/DH/DSA (also symmetric/PQC).",
335
+ algorithms: ["RSA", "ECDH", "ECDSA", "DSA"],
336
+ severity: "medium",
337
+ },
338
+ {
339
+ name: "pyjwt",
340
+ ecosystem: "pypi",
341
+ reason: "JWT signing with classical RS*/ES* algorithms.",
342
+ algorithms: ["RSA", "ECDSA"],
343
+ severity: "medium",
344
+ },
345
+ {
346
+ name: "python-jose",
347
+ ecosystem: "pypi",
348
+ reason: "JOSE/JWT with classical RSA/ECDSA algorithms.",
349
+ algorithms: ["RSA", "ECDSA"],
350
+ severity: "medium",
351
+ },
352
+ {
353
+ name: "paramiko",
354
+ ecosystem: "pypi",
355
+ reason: "SSH client/server with classical RSA/ECDSA/Ed25519/DSA host + user keys.",
356
+ algorithms: ["RSA", "ECDSA", "EdDSA", "DSA"],
357
+ severity: "medium",
358
+ },
359
+ {
360
+ name: "pyopenssl",
361
+ ecosystem: "pypi",
362
+ reason: "OpenSSL bindings for classical RSA/EC X.509 + TLS.",
363
+ algorithms: ["RSA", "ECDSA"],
364
+ severity: "medium",
365
+ },
366
+ {
367
+ name: "pynacl",
368
+ ecosystem: "pypi",
369
+ reason:
370
+ "libsodium bindings — X25519 key agreement and Ed25519 signatures (modern but classical).",
371
+ algorithms: ["X25519", "EdDSA"],
372
+ severity: "low",
373
+ },
374
+
375
+ // --- crates.io (Rust) ---
376
+ {
377
+ name: "rsa",
378
+ ecosystem: "cargo",
379
+ reason: "Pure-Rust RSA encryption and signing.",
380
+ algorithms: ["RSA"],
381
+ severity: "high",
382
+ },
383
+ {
384
+ name: "ring",
385
+ ecosystem: "cargo",
386
+ reason: "RSA / ECDSA / Ed25519 / ECDH primitives.",
387
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECDH"],
388
+ severity: "medium",
389
+ },
390
+ {
391
+ name: "openssl",
392
+ ecosystem: "cargo",
393
+ reason: "OpenSSL bindings for classical RSA/EC crypto.",
394
+ algorithms: ["RSA", "ECDSA"],
395
+ severity: "medium",
396
+ },
397
+ {
398
+ name: "p256",
399
+ ecosystem: "cargo",
400
+ reason: "NIST P-256 ECDSA signatures and ECDH key agreement.",
401
+ algorithms: ["ECDSA", "ECDH"],
402
+ severity: "high",
403
+ },
404
+ {
405
+ name: "p384",
406
+ ecosystem: "cargo",
407
+ reason: "NIST P-384 ECDSA signatures and ECDH key agreement.",
408
+ algorithms: ["ECDSA", "ECDH"],
409
+ severity: "high",
410
+ },
411
+ {
412
+ name: "k256",
413
+ ecosystem: "cargo",
414
+ reason: "secp256k1 ECDSA signatures and ECDH key agreement.",
415
+ algorithms: ["ECDSA", "ECDH"],
416
+ severity: "high",
417
+ },
418
+ {
419
+ name: "ed25519-dalek",
420
+ ecosystem: "cargo",
421
+ reason: "Ed25519 signatures (modern but classical).",
422
+ algorithms: ["EdDSA"],
423
+ severity: "low",
424
+ },
425
+ {
426
+ name: "x25519-dalek",
427
+ ecosystem: "cargo",
428
+ reason: "X25519 key agreement (modern but classical).",
429
+ algorithms: ["X25519"],
430
+ severity: "low",
431
+ },
432
+
433
+ // --- Go modules ---
434
+ {
435
+ name: "golang.org/x/crypto",
436
+ ecosystem: "go",
437
+ reason: "SSH, OpenPGP, and classical curve helpers on top of the Go stdlib.",
438
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECDH"],
439
+ severity: "medium",
440
+ },
441
+
442
+ // --- Maven (Java) ---
443
+ {
444
+ name: "bcprov-jdk18on",
445
+ ecosystem: "maven",
446
+ reason: "BouncyCastle provider — full classical RSA/ECDSA/ECDH/DSA suite.",
447
+ algorithms: ["RSA", "ECDSA", "ECDH", "DSA"],
448
+ severity: "high",
449
+ },
450
+ {
451
+ name: "bcprov-jdk15on",
452
+ ecosystem: "maven",
453
+ reason: "BouncyCastle provider (JDK 1.5+) — classical RSA/ECDSA/ECDH/DSA.",
454
+ algorithms: ["RSA", "ECDSA", "ECDH", "DSA"],
455
+ severity: "high",
456
+ },
457
+ {
458
+ name: "bcpkix-jdk18on",
459
+ ecosystem: "maven",
460
+ reason: "BouncyCastle PKIX — classical X.509 / CMS with RSA/ECDSA.",
461
+ algorithms: ["RSA", "ECDSA"],
462
+ severity: "medium",
463
+ },
464
+ {
465
+ name: "java-jwt",
466
+ ecosystem: "maven",
467
+ reason: "Auth0 JWT with classical RS*/ES* algorithms.",
468
+ algorithms: ["RSA", "ECDSA"],
469
+ severity: "medium",
470
+ },
471
+
472
+ // --- RubyGems ---
473
+ {
474
+ name: "jwt",
475
+ ecosystem: "rubygems",
476
+ reason: "Ruby JWT with classical RS*/ES* algorithms.",
477
+ algorithms: ["RSA", "ECDSA"],
478
+ severity: "medium",
479
+ },
480
+ {
481
+ name: "rbnacl",
482
+ ecosystem: "rubygems",
483
+ reason: "libsodium bindings — X25519 key agreement and Ed25519 signatures.",
484
+ algorithms: ["X25519", "EdDSA"],
485
+ severity: "low",
486
+ },
487
+ {
488
+ name: "ed25519",
489
+ ecosystem: "rubygems",
490
+ reason: "Ed25519 signatures (modern but classical).",
491
+ algorithms: ["EdDSA"],
492
+ severity: "low",
493
+ },
262
494
  ];
263
495
 
264
- /** Fast lookup map by package name. */
265
- const BY_NAME = new Map<string, VulnerableDependency>(
266
- vulnerableDependencies.map((d) => [d.name, d]),
267
- );
496
+ /**
497
+ * Normalise a package name for matching within its ecosystem. PyPI is
498
+ * case-insensitive and folds runs of `-_.` to a single `-` (PEP 503); cargo /
499
+ * maven / rubygems are effectively lower-case; npm and go module paths are
500
+ * matched verbatim (npm scopes and go paths are case-sensitive).
501
+ */
502
+ function normalizeName(ecosystem: DependencyEcosystem, name: string): string {
503
+ const n = name.trim();
504
+ if (ecosystem === "pypi") return n.toLowerCase().replace(/[-_.]+/g, "-");
505
+ if (ecosystem === "npm" || ecosystem === "go") return n;
506
+ return n.toLowerCase();
507
+ }
508
+
509
+ /** ecosystem → (normalised name → entry). Scoped so `rsa` (pypi) ≠ `rsa` (cargo). */
510
+ const BY_ECOSYSTEM: Map<DependencyEcosystem, Map<string, VulnerableDependency>> = (() => {
511
+ const m = new Map<DependencyEcosystem, Map<string, VulnerableDependency>>();
512
+ for (const d of vulnerableDependencies) {
513
+ let em = m.get(d.ecosystem);
514
+ if (!em) {
515
+ em = new Map();
516
+ m.set(d.ecosystem, em);
517
+ }
518
+ em.set(normalizeName(d.ecosystem, d.name), d);
519
+ }
520
+ return m;
521
+ })();
268
522
 
269
523
  /**
270
- * Precompiled `"name":` lookup regex per known package (P1-9): the package set
271
- * is static, so we build the (escaped-name → RegExp) map once at module load
272
- * rather than recompiling a fresh RegExp per found package per manifest.
524
+ * Precompiled `"name":` lookup regex per known npm package (P1-9): the set is
525
+ * static, so build the (name → RegExp) map once rather than per manifest. Only
526
+ * npm uses the JSON-key form; other ecosystems locate names generically.
273
527
  */
274
528
  const KEY_REGEX_BY_NAME = new Map<string, RegExp>(
275
- vulnerableDependencies.map((d): [string, RegExp] => {
276
- const escaped = d.name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
277
- return [d.name, new RegExp(`"${escaped}"\\s*:`)];
278
- }),
529
+ vulnerableDependencies
530
+ .filter((d) => d.ecosystem === "npm")
531
+ .map((d): [string, RegExp] => {
532
+ const escaped = d.name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
533
+ return [d.name, new RegExp(`"${escaped}"\\s*:`)];
534
+ }),
279
535
  );
280
536
 
281
- /** Algorithm families that expose confidentiality (HNDL) rather than only signing. */
282
- const CONFIDENTIALITY_FAMILIES: ReadonlySet<AlgorithmFamily> = new Set<AlgorithmFamily>([
283
- "RSA",
284
- "ECDH",
285
- "DH",
286
- "ECIES",
287
- "X25519",
288
- "X448",
289
- ]);
290
-
291
537
  /**
292
538
  * Build a remediation string covering ALL families a package exposes (C5),
293
- * rather than only `algorithms[0]`. De-duplicates the per-family recommendation
294
- * strings and joins them, so a signature+KEM library points at both replacements.
539
+ * rather than only `algorithms[0]`. De-duplicates by PQC *target* — the KEM
540
+ * target and the signature target are each named at most once — so a library
541
+ * exposing both a confidentiality family and a signature family reads
542
+ * "ML-KEM-768 … for key exchange/encryption; ML-DSA-65 (FIPS 204) for
543
+ * signatures" instead of repeating ML-DSA (the old per-string Set couldn't
544
+ * collapse "ML-DSA-65 for signatures" against a standalone "ML-DSA-65 (FIPS 204)").
295
545
  */
296
546
  function multiFamilyRemediation(algorithms: readonly AlgorithmFamily[]): string {
297
- const parts = new Set<string>();
298
- for (const a of algorithms) parts.add(remediationText(a));
299
- return [...parts].join("; ");
547
+ let needsKem = false;
548
+ let needsSig = false;
549
+ let needsReview = false;
550
+ for (const a of algorithms) {
551
+ if (isConfidentialityFamily(a)) needsKem = true;
552
+ if (isSignatureFamily(a)) needsSig = true;
553
+ if (a === "unknown") needsReview = true;
554
+ }
555
+ const parts: string[] = [];
556
+ if (needsKem)
557
+ parts.push("ML-KEM-768 (FIPS 203, hybrid X25519MLKEM768) for key exchange/encryption");
558
+ if (needsSig) parts.push("ML-DSA-65 (FIPS 204) for signatures");
559
+ // Only fall back to the generic "review" line when nothing concrete applied.
560
+ if (needsReview && parts.length === 0) parts.push(remediationText("unknown"));
561
+ return parts.join("; ");
562
+ }
563
+
564
+ /**
565
+ * The dependency ecosystem a manifest filename belongs to, or null when the file
566
+ * is not a manifest we parse. Drives both {@link isManifestFile} and the scanner
567
+ * dispatch — one place to add a manifest format.
568
+ */
569
+ export function manifestEcosystem(file: string): DependencyEcosystem | null {
570
+ const base = (file.split("/").pop() ?? file).toLowerCase();
571
+ if (
572
+ base === "package.json" ||
573
+ base === "package-lock.json" ||
574
+ base === "npm-shrinkwrap.json" ||
575
+ base === "yarn.lock" ||
576
+ base === "pnpm-lock.yaml"
577
+ ) {
578
+ return "npm";
579
+ }
580
+ if (base === "requirements.txt" || /^requirements[\w.-]*\.txt$/.test(base)) return "pypi";
581
+ if (base === "pyproject.toml" || base === "pipfile") return "pypi";
582
+ if (base === "cargo.toml") return "cargo";
583
+ if (base === "go.mod") return "go";
584
+ if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") return "maven";
585
+ if (base === "gemfile" || base.endsWith(".gemspec")) return "rubygems";
586
+ return null;
300
587
  }
301
588
 
302
589
  /** True if a file path looks like a manifest we can parse for dependencies. */
303
590
  export function isManifestFile(file: string): boolean {
304
- const base = file.split("/").pop() ?? file;
305
- return base === "package.json" || base === "package-lock.json";
591
+ return manifestEcosystem(file) !== null;
592
+ }
593
+
594
+ /**
595
+ * Extract candidate package names from a non-npm manifest. Extraction is
596
+ * deliberately GENEROUS — every finding still has to match a curated DB entry,
597
+ * so over-extracting harmless tokens (e.g. a TOML `version = …` key) costs
598
+ * nothing while keeping the parsers tiny and format-tolerant.
599
+ */
600
+ function candidateNames(ecosystem: DependencyEcosystem, content: string): string[] {
601
+ const names: string[] = [];
602
+ const lines = content.split("\n");
603
+ switch (ecosystem) {
604
+ case "pypi": {
605
+ // requirements.txt / Pipfile / pyproject.toml: leading token per line, plus
606
+ // quoted deps in arrays/tables. Skip comments and `-r/-e/--flag` lines.
607
+ for (const raw of lines) {
608
+ const line = raw.trim();
609
+ if (!line || line.startsWith("#") || line.startsWith("-")) continue;
610
+ const lead = /^["']?([A-Za-z][A-Za-z0-9._-]+)/.exec(line);
611
+ if (lead) names.push(lead[1]);
612
+ for (const m of line.matchAll(/["']([A-Za-z][A-Za-z0-9._-]+)/g)) names.push(m[1]);
613
+ }
614
+ break;
615
+ }
616
+ case "cargo": {
617
+ // Cargo.toml: dependency-table keys `name = …` and `[…dependencies.name]`.
618
+ for (const raw of lines) {
619
+ const line = raw.trim();
620
+ if (!line || line.startsWith("#")) continue;
621
+ const key = /^([A-Za-z0-9][A-Za-z0-9_-]+)\s*=/.exec(line);
622
+ if (key) names.push(key[1]);
623
+ const table = /^\[[\w.-]*dependencies\.([A-Za-z0-9][A-Za-z0-9_-]+)\]/.exec(line);
624
+ if (table) names.push(table[1]);
625
+ }
626
+ break;
627
+ }
628
+ case "go": {
629
+ // go.mod: `require path v1.2.3` (single or in a require( … ) block).
630
+ for (const m of content.matchAll(/(?:^|\s)([a-z0-9][\w.\-/]+)\s+v\d/gm)) names.push(m[1]);
631
+ break;
632
+ }
633
+ case "maven": {
634
+ // pom.xml <artifactId> tags + gradle `group:artifact:version` strings.
635
+ for (const m of content.matchAll(/<artifactId>\s*([\w.-]+)\s*<\/artifactId>/g)) {
636
+ names.push(m[1]);
637
+ }
638
+ for (const m of content.matchAll(/["']([\w.-]+):([\w.-]+):[\w.$-]+["']/g)) names.push(m[2]);
639
+ break;
640
+ }
641
+ case "rubygems": {
642
+ // Gemfile `gem 'name'` + gemspec `add_*dependency 'name'`.
643
+ for (const m of content.matchAll(/\bgem\s+["']([\w.-]+)["']/g)) names.push(m[1]);
644
+ for (const m of content.matchAll(
645
+ /add(?:_runtime|_development)?_dependency\s+["']([\w.-]+)["']/g,
646
+ )) {
647
+ names.push(m[1]);
648
+ }
649
+ break;
650
+ }
651
+ case "npm":
652
+ break; // npm uses the JSON path, not this extractor.
653
+ }
654
+ return names;
655
+ }
656
+
657
+ /** First offset of a package `name` as a whole token in the manifest text (or 0). */
658
+ function offsetOfName(content: string, name: string): number {
659
+ const escaped = name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
660
+ const m = new RegExp(`(?<![\\w./-])${escaped}(?![\\w-])`).exec(content);
661
+ return m ? m.index : 0;
306
662
  }
307
663
 
308
664
  /**
@@ -327,7 +683,7 @@ function dependencyFinding(
327
683
  confidence: "high",
328
684
  algorithm,
329
685
  // Confidentiality libs are HNDL-exposed; signature-only ones are not.
330
- hndl: dep.algorithms.some((a) => CONFIDENTIALITY_FAMILIES.has(a)),
686
+ hndl: dep.algorithms.some(isConfidentialityFamily),
331
687
  cwe: CWE_BROKEN_CRYPTO,
332
688
  message: `${dep.name} — ${dep.reason}`,
333
689
  remediation: multiFamilyRemediation(dep.algorithms),
@@ -346,16 +702,56 @@ function offsetOfKey(content: string, name: string): number {
346
702
  return m ? m.index : 0;
347
703
  }
348
704
 
705
+ /** Sort findings by title for deterministic output. */
706
+ function sortByTitle(findings: Finding[]): Finding[] {
707
+ findings.sort((a, b) => (a.title < b.title ? -1 : a.title > b.title ? 1 : 0));
708
+ return findings;
709
+ }
710
+
349
711
  /**
350
- * Scan a single manifest file's contents for vulnerable dependencies.
351
- *
352
- * - `package.json`: dependencies / devDependencies / peerDependencies / optionalDependencies.
353
- * - `package-lock.json` (v2/v3): the `packages` map keys (node_modules/<name>),
354
- * plus legacy `dependencies` map for v1.
355
- *
356
- * Returns one finding per distinct vulnerable package name found in the file.
712
+ * Scan a single manifest file for vulnerable dependencies, dispatching by the
713
+ * file's ecosystem ({@link manifestEcosystem}). npm manifests are parsed as
714
+ * JSON; every other ecosystem uses the generous {@link candidateNames} extractor
715
+ * filtered against the ecosystem-scoped database. Returns one finding per
716
+ * distinct vulnerable package.
357
717
  */
358
718
  export function scanManifest(file: string, content: string): Finding[] {
719
+ const ecosystem = manifestEcosystem(file);
720
+ if (!ecosystem) return [];
721
+ const db = BY_ECOSYSTEM.get(ecosystem);
722
+ if (!db) return [];
723
+
724
+ if (ecosystem === "npm") {
725
+ const base = (file.split("/").pop() ?? file).toLowerCase();
726
+ // yarn.lock / pnpm-lock.yaml are not JSON — extract names from their text.
727
+ if (base === "yarn.lock" || base === "pnpm-lock.yaml") {
728
+ return scanNpmLockfile(content, file, db);
729
+ }
730
+ return scanNpmManifest(content, file, db);
731
+ }
732
+
733
+ const found = new Map<string, VulnerableDependency>(); // dedupe by entry
734
+ for (const raw of candidateNames(ecosystem, content)) {
735
+ const dep = db.get(normalizeName(ecosystem, raw));
736
+ if (dep) found.set(dep.name, dep);
737
+ }
738
+ const findings: Finding[] = [];
739
+ for (const dep of found.values()) {
740
+ findings.push(dependencyFinding(dep, file, content, offsetOfName(content, dep.name)));
741
+ }
742
+ return sortByTitle(findings);
743
+ }
744
+
745
+ /**
746
+ * npm manifest scan.
747
+ * - `package.json`: dependencies / devDependencies / peerDependencies / optionalDependencies.
748
+ * - `package-lock.json` (v2/v3): the `packages` map keys (node_modules/<name>).
749
+ */
750
+ function scanNpmManifest(
751
+ content: string,
752
+ file: string,
753
+ db: Map<string, VulnerableDependency>,
754
+ ): Finding[] {
359
755
  let json: unknown;
360
756
  try {
361
757
  json = JSON.parse(content);
@@ -370,17 +766,15 @@ export function scanManifest(file: string, content: string): Finding[] {
370
766
  const collectFromRecord = (rec: unknown): void => {
371
767
  if (rec === null || typeof rec !== "object") return;
372
768
  for (const key of Object.keys(rec as Record<string, unknown>)) {
373
- if (BY_NAME.has(key)) found.add(key);
769
+ if (db.has(key)) found.add(key);
374
770
  }
375
771
  };
376
772
 
377
- // package.json dependency sections.
378
773
  collectFromRecord(obj.dependencies);
379
774
  collectFromRecord(obj.devDependencies);
380
775
  collectFromRecord(obj.peerDependencies);
381
776
  collectFromRecord(obj.optionalDependencies);
382
777
 
383
- // package-lock.json v2/v3 "packages" map: keys are "node_modules/<name>".
384
778
  const packages = obj.packages;
385
779
  if (packages !== null && typeof packages === "object") {
386
780
  for (const key of Object.keys(packages as Record<string, unknown>)) {
@@ -388,17 +782,55 @@ export function scanManifest(file: string, content: string): Finding[] {
388
782
  const marker = "node_modules/";
389
783
  const idx = key.lastIndexOf(marker);
390
784
  const name = idx >= 0 ? key.slice(idx + marker.length) : key;
391
- if (BY_NAME.has(name)) found.add(name);
785
+ if (db.has(name)) found.add(name);
392
786
  }
393
787
  }
394
788
 
395
789
  const findings: Finding[] = [];
396
790
  for (const name of found) {
397
- const dep = BY_NAME.get(name);
791
+ const dep = db.get(name);
398
792
  if (!dep) continue;
399
793
  findings.push(dependencyFinding(dep, file, content, offsetOfKey(content, name)));
400
794
  }
401
- // Deterministic ordering by package name.
402
- findings.sort((a, b) => (a.title < b.title ? -1 : a.title > b.title ? 1 : 0));
403
- return findings;
795
+ return sortByTitle(findings);
796
+ }
797
+
798
+ /**
799
+ * Extract candidate package names from a yarn.lock / pnpm-lock.yaml text body.
800
+ * Generous — every `name@…` (and pnpm's `/name/version`) token is captured; the
801
+ * caller filters against the vulnerable-dependency DB, so over-capture is safe.
802
+ * Handles yarn v1 (`elliptic@^6.5.4:`), yarn berry (`"elliptic@npm:^6.5.4":`),
803
+ * and pnpm (`/elliptic@6.5.4:`, scoped `/@noble/curves@1.2.0:`).
804
+ */
805
+ function npmLockfileCandidates(content: string): string[] {
806
+ const names = new Set<string>();
807
+ const at = /(?:^|[\s,"'/])((?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*)@/gm;
808
+ let m: RegExpExecArray | null;
809
+ while ((m = at.exec(content)) !== null) names.add(m[1].toLowerCase());
810
+ // pnpm's older `/name/version` form (name followed by a version digit).
811
+ const slash = /(?:^|\s)\/((?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*)\/\d/gm;
812
+ while ((m = slash.exec(content)) !== null) names.add(m[1].toLowerCase());
813
+ return [...names];
814
+ }
815
+
816
+ /**
817
+ * npm lockfile scan (yarn.lock / pnpm-lock.yaml): match every package token in
818
+ * the lockfile text against the vulnerable-dependency DB. Catches transitive
819
+ * dependencies that never appear in package.json.
820
+ */
821
+ function scanNpmLockfile(
822
+ content: string,
823
+ file: string,
824
+ db: Map<string, VulnerableDependency>,
825
+ ): Finding[] {
826
+ const found = new Map<string, VulnerableDependency>();
827
+ for (const name of npmLockfileCandidates(content)) {
828
+ const dep = db.get(name);
829
+ if (dep) found.set(dep.name, dep);
830
+ }
831
+ const findings: Finding[] = [];
832
+ for (const dep of found.values()) {
833
+ findings.push(dependencyFinding(dep, file, content, offsetOfName(content, dep.name)));
834
+ }
835
+ return sortByTitle(findings);
404
836
  }