@quantakrypto/core 0.5.0 → 0.6.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 (46) hide show
  1. package/dist/crypto-agility.d.ts +158 -0
  2. package/dist/crypto-agility.d.ts.map +1 -0
  3. package/dist/crypto-agility.js +285 -0
  4. package/dist/crypto-agility.js.map +1 -0
  5. package/dist/dependencies.d.ts.map +1 -1
  6. package/dist/dependencies.js +180 -18
  7. package/dist/dependencies.js.map +1 -1
  8. package/dist/detectors/source.d.ts.map +1 -1
  9. package/dist/detectors/source.js +52 -0
  10. package/dist/detectors/source.js.map +1 -1
  11. package/dist/hndl.d.ts +241 -0
  12. package/dist/hndl.d.ts.map +1 -0
  13. package/dist/hndl.js +752 -0
  14. package/dist/hndl.js.map +1 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/parallel.d.ts.map +1 -1
  20. package/dist/parallel.js +22 -23
  21. package/dist/parallel.js.map +1 -1
  22. package/dist/report.d.ts +8 -0
  23. package/dist/report.d.ts.map +1 -1
  24. package/dist/report.js +75 -19
  25. package/dist/report.js.map +1 -1
  26. package/dist/types.d.ts +1 -1
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/types.js.map +1 -1
  29. package/dist/version.d.ts +1 -1
  30. package/dist/version.js +1 -1
  31. package/dist/version.js.map +1 -1
  32. package/dist/walk.d.ts +31 -1
  33. package/dist/walk.d.ts.map +1 -1
  34. package/dist/walk.js +30 -9
  35. package/dist/walk.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/crypto-agility.ts +407 -0
  38. package/src/dependencies.ts +182 -16
  39. package/src/detectors/source.ts +62 -0
  40. package/src/hndl.ts +1012 -0
  41. package/src/index.ts +52 -0
  42. package/src/parallel.ts +21 -20
  43. package/src/report.ts +84 -19
  44. package/src/types.ts +9 -1
  45. package/src/version.ts +1 -1
  46. package/src/walk.ts +47 -10
@@ -401,6 +401,14 @@ export const vulnerableDependencies: VulnerableDependency[] = [
401
401
  algorithms: ["X25519", "EdDSA"],
402
402
  severity: "low",
403
403
  },
404
+ {
405
+ name: "tink",
406
+ ecosystem: "pypi",
407
+ reason:
408
+ "Google Tink (Python) — classical RSA/ECDSA/Ed25519 signatures and ECIES hybrid encryption.",
409
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
410
+ severity: "high",
411
+ },
404
412
 
405
413
  // --- crates.io (Rust) ---
406
414
  {
@@ -513,6 +521,21 @@ export const vulnerableDependencies: VulnerableDependency[] = [
513
521
  algorithms: ["ECDSA", "ECDH"],
514
522
  severity: "medium",
515
523
  },
524
+ {
525
+ name: "github.com/tink-crypto/tink-go/v2",
526
+ ecosystem: "go",
527
+ reason:
528
+ "Google Tink (Go) — classical RSA/ECDSA/Ed25519 signatures and ECIES hybrid encryption.",
529
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
530
+ severity: "high",
531
+ },
532
+ {
533
+ name: "github.com/google/tink/go",
534
+ ecosystem: "go",
535
+ reason: "Google Tink (legacy Go module) — classical RSA/ECDSA/Ed25519 and ECIES hybrid.",
536
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
537
+ severity: "high",
538
+ },
516
539
 
517
540
  // --- Maven (Java) ---
518
541
  {
@@ -559,6 +582,14 @@ export const vulnerableDependencies: VulnerableDependency[] = [
559
582
  severity: "medium",
560
583
  hndl: false,
561
584
  },
585
+ {
586
+ name: "tink",
587
+ ecosystem: "maven",
588
+ reason:
589
+ "Google Tink (com.google.crypto.tink) — classical RSA/ECDSA/Ed25519 signatures and ECIES hybrid encryption.",
590
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
591
+ severity: "high",
592
+ },
562
593
 
563
594
  // --- RubyGems ---
564
595
  {
@@ -623,14 +654,83 @@ export const vulnerableDependencies: VulnerableDependency[] = [
623
654
  severity: "medium",
624
655
  hndl: false,
625
656
  },
657
+
658
+ // --- Composer (PHP) ---
659
+ {
660
+ name: "phpseclib/phpseclib",
661
+ ecosystem: "composer",
662
+ reason: "Pure-PHP RSA / DSA / ECDSA / Diffie-Hellman public-key crypto and X.509.",
663
+ algorithms: ["RSA", "DSA", "ECDSA", "DH"],
664
+ severity: "high",
665
+ },
666
+ {
667
+ name: "paragonie/sodium_compat",
668
+ ecosystem: "composer",
669
+ reason: "Pure-PHP libsodium polyfill: X25519 key agreement and Ed25519 signatures.",
670
+ algorithms: ["X25519", "EdDSA"],
671
+ severity: "low",
672
+ },
673
+ {
674
+ name: "paragonie/halite",
675
+ ecosystem: "composer",
676
+ reason: "libsodium wrapper: X25519 key agreement and Ed25519 signatures.",
677
+ algorithms: ["X25519", "EdDSA"],
678
+ severity: "low",
679
+ },
680
+ {
681
+ name: "paragonie/paseto",
682
+ ecosystem: "composer",
683
+ reason: "PASETO public tokens signed with classical Ed25519 (v2/v4) or RSA.",
684
+ algorithms: ["EdDSA", "RSA"],
685
+ severity: "medium",
686
+ hndl: false,
687
+ },
688
+ {
689
+ name: "firebase/php-jwt",
690
+ ecosystem: "composer",
691
+ reason: "PHP JWT signing/verification with classical RS*/ES* algorithms.",
692
+ algorithms: ["RSA", "ECDSA"],
693
+ severity: "medium",
694
+ hndl: false,
695
+ },
696
+ {
697
+ name: "lcobucci/jwt",
698
+ ecosystem: "composer",
699
+ reason: "PHP JWT with classical RSA/ECDSA signature algorithms.",
700
+ algorithms: ["RSA", "ECDSA"],
701
+ severity: "medium",
702
+ hndl: false,
703
+ },
704
+ {
705
+ name: "web-token/jwt-framework",
706
+ ecosystem: "composer",
707
+ reason: "JOSE (JWS/JWE) framework: classical RSA/ECDSA signatures and ECDH-ES key agreement.",
708
+ algorithms: ["RSA", "ECDSA", "ECDH"],
709
+ severity: "medium",
710
+ },
711
+ {
712
+ name: "mdanter/ecc",
713
+ ecosystem: "composer",
714
+ reason: "Pure-PHP elliptic-curve ECDSA signatures and ECDH key agreement.",
715
+ algorithms: ["ECDSA", "ECDH"],
716
+ severity: "high",
717
+ },
718
+ {
719
+ name: "simplito/elliptic-php",
720
+ ecosystem: "composer",
721
+ reason: "secp256k1 / NIST-curve ECDSA and ECDH in pure PHP (blockchain keys).",
722
+ algorithms: ["ECDSA", "ECDH"],
723
+ severity: "high",
724
+ },
626
725
  ];
627
726
 
628
727
  /**
629
728
  * Normalise a package name for matching within its ecosystem. PyPI is
630
729
  * case-insensitive and folds runs of `-_.` to a single `-` (PEP 503); cargo /
631
- * maven / rubygems / nuget are effectively lower-case (NuGet ids are matched
632
- * case-insensitively); npm and go module paths are matched verbatim (npm scopes
633
- * and go paths are case-sensitive).
730
+ * maven / rubygems / nuget / composer are effectively lower-case (NuGet ids are
731
+ * matched case-insensitively; Composer `vendor/package` names are lower-cased by
732
+ * Packagist); npm and go module paths are matched verbatim (npm scopes and go
733
+ * paths are case-sensitive).
634
734
  */
635
735
  function normalizeName(ecosystem: DependencyEcosystem, name: string): string {
636
736
  const n = name.trim();
@@ -718,6 +818,7 @@ export function manifestEcosystem(file: string): DependencyEcosystem | null {
718
818
  if (base === "gemfile" || base.endsWith(".gemspec")) return "rubygems";
719
819
  if (base === "packages.config" || base === "directory.packages.props" || base.endsWith(".csproj"))
720
820
  return "nuget";
821
+ if (base === "composer.json" || base === "composer.lock") return "composer";
721
822
  return null;
722
823
  }
723
824
 
@@ -797,6 +898,20 @@ function candidateNames(ecosystem: DependencyEcosystem, content: string): string
797
898
  }
798
899
  break;
799
900
  }
901
+ case "composer": {
902
+ // composer.json `require`/`require-dev` maps are keyed by "vendor/package";
903
+ // composer.lock lists `{"name": "vendor/package", …}`. Both are JSON, so
904
+ // grab every quoted `vendor/package` token (a lower-case vendor + package
905
+ // separated by a single `/`). PHP platform requirements ("php", "ext-*")
906
+ // carry no slash and so are naturally excluded; the curated DB filters the
907
+ // rest, so over-capture is safe.
908
+ for (const m of content.matchAll(
909
+ /["']([a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\/[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)["']/gi,
910
+ )) {
911
+ names.push(m[1]);
912
+ }
913
+ break;
914
+ }
800
915
  case "npm":
801
916
  break; // npm uses the JSON path, not this extractor.
802
917
  }
@@ -893,10 +1008,33 @@ export function scanManifest(file: string, content: string): Finding[] {
893
1008
  return sortByTitle(findings);
894
1009
  }
895
1010
 
1011
+ /**
1012
+ * Resolve an npm `npm:` alias spec to the real package it installs, or null when
1013
+ * the spec is not an alias. An aliased dependency records the alias as the map
1014
+ * KEY and `npm:<real-name>@<range>` as the VALUE (in package.json and in a
1015
+ * lockfile entry's `version`), so the vulnerable package hides behind a benign
1016
+ * alias name. Keeps a leading scope `@` (only a version `@` past index 0 is a
1017
+ * separator): `npm:node-forge@^1` → `node-forge`, `npm:@scope/pkg@1` → `@scope/pkg`.
1018
+ */
1019
+ function npmAliasTarget(spec: string): string | null {
1020
+ if (!spec.startsWith("npm:")) return null;
1021
+ const rest = spec.slice(4);
1022
+ const at = rest.lastIndexOf("@");
1023
+ const name = at > 0 ? rest.slice(0, at) : rest;
1024
+ return name || null;
1025
+ }
1026
+
1027
+ /** Guard against a pathological / hostile lockfile nesting the walk unbounded. */
1028
+ const NPM_TREE_MAX_DEPTH = 64;
1029
+
896
1030
  /**
897
1031
  * npm manifest scan.
898
- * - `package.json`: dependencies / devDependencies / peerDependencies / optionalDependencies.
899
- * - `package-lock.json` (v2/v3): the `packages` map keys (node_modules/<name>).
1032
+ * - `package.json`: dependencies / devDependencies / peerDependencies / optionalDependencies,
1033
+ * including `npm:`-aliased entries (the alias key hides the real package name).
1034
+ * - `package-lock.json` v1: the NESTED `dependencies` tree, where each entry may carry
1035
+ * its own `dependencies` (transitive deps) and an `npm:` alias in `version`.
1036
+ * - `package-lock.json` v2/v3: the flat `packages` map keys (node_modules/<name>),
1037
+ * plus each entry's own `name` field (set for aliased installs).
900
1038
  */
901
1039
  function scanNpmManifest(
902
1040
  content: string,
@@ -914,26 +1052,54 @@ function scanNpmManifest(
914
1052
  const found = new Set<string>();
915
1053
  const obj = json as Record<string, unknown>;
916
1054
 
917
- const collectFromRecord = (rec: unknown): void => {
918
- if (rec === null || typeof rec !== "object") return;
919
- for (const key of Object.keys(rec as Record<string, unknown>)) {
920
- if (db.has(key)) found.add(key);
1055
+ const add = (name: string): void => {
1056
+ if (db.has(name)) found.add(name);
1057
+ };
1058
+
1059
+ /**
1060
+ * Walk a dependency map that may be either a flat package.json map
1061
+ * (name → version-range string) or a legacy package-lock v1 tree (name →
1062
+ * `{ version, requires, dependencies }`, arbitrarily nested). Matches keys,
1063
+ * resolves `npm:` aliases from string values and entry `version` fields, and
1064
+ * recurses into a nested `dependencies` sub-tree.
1065
+ */
1066
+ const walkDepTree = (deps: unknown, depth: number): void => {
1067
+ if (depth > NPM_TREE_MAX_DEPTH || deps === null || typeof deps !== "object") return;
1068
+ for (const [name, node] of Object.entries(deps as Record<string, unknown>)) {
1069
+ add(name);
1070
+ if (typeof node === "string") {
1071
+ // package.json form: the value is a range, possibly `npm:<real>@<range>`.
1072
+ const aliased = npmAliasTarget(node);
1073
+ if (aliased) add(aliased);
1074
+ } else if (node !== null && typeof node === "object") {
1075
+ const rec = node as Record<string, unknown>;
1076
+ if (typeof rec.version === "string") {
1077
+ const aliased = npmAliasTarget(rec.version);
1078
+ if (aliased) add(aliased);
1079
+ }
1080
+ walkDepTree(rec.dependencies, depth + 1); // v1 nests transitive deps here
1081
+ }
921
1082
  }
922
1083
  };
923
1084
 
924
- collectFromRecord(obj.dependencies);
925
- collectFromRecord(obj.devDependencies);
926
- collectFromRecord(obj.peerDependencies);
927
- collectFromRecord(obj.optionalDependencies);
1085
+ walkDepTree(obj.dependencies, 0);
1086
+ walkDepTree(obj.devDependencies, 0);
1087
+ walkDepTree(obj.peerDependencies, 0);
1088
+ walkDepTree(obj.optionalDependencies, 0);
928
1089
 
929
1090
  const packages = obj.packages;
930
1091
  if (packages !== null && typeof packages === "object") {
931
- for (const key of Object.keys(packages as Record<string, unknown>)) {
1092
+ for (const [key, entry] of Object.entries(packages as Record<string, unknown>)) {
932
1093
  if (!key) continue; // root package entry
933
1094
  const marker = "node_modules/";
934
1095
  const idx = key.lastIndexOf(marker);
935
- const name = idx >= 0 ? key.slice(idx + marker.length) : key;
936
- if (db.has(name)) found.add(name);
1096
+ add(idx >= 0 ? key.slice(idx + marker.length) : key);
1097
+ // Aliased install: the entry's own `name` field is the real package, which
1098
+ // differs from the alias embedded in the node_modules/<alias> key.
1099
+ if (entry !== null && typeof entry === "object") {
1100
+ const realName = (entry as Record<string, unknown>).name;
1101
+ if (typeof realName === "string") add(realName);
1102
+ }
937
1103
  }
938
1104
  }
939
1105
 
@@ -47,6 +47,15 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
47
47
  // (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
48
48
  const RE_GENERATE_KEYPAIR =
49
49
  /generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
50
+ // Variable-typed key generation: `generateKeyPair(kind, …)` where the algorithm
51
+ // argument is an IDENTIFIER (a variable), not a quoted literal, so the concrete
52
+ // family is unknown at scan time but a classical key is still being generated.
53
+ // The lookbehind anchors the call to a word boundary so it does not fire inside
54
+ // a longer identifier (e.g. `myGenerateKeyPair(`); the first argument must be a
55
+ // bare identifier immediately followed by `,` or `)` so a quoted-literal call
56
+ // (handled by RE_GENERATE_KEYPAIR) never matches here.
57
+ const RE_GENERATE_KEYPAIR_VAR =
58
+ /(?<![\w$])generateKeyPair(?:Sync)?\s*\(\s*([A-Za-z_$][\w$]*)\s*[,)]/g;
50
59
 
51
60
  /** Per-key-type classification for `generateKeyPair(Sync)('<type>', …)`. Hoisted
52
61
  * to module scope so the direct matcher AND the import-alias pass (below) share
@@ -177,6 +186,20 @@ function escapeRe(s: string): string {
177
186
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
178
187
  }
179
188
  const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
189
+ // Computed-member (bracket) access to a Node `crypto` method:
190
+ // `crypto['createSign'](…)`, `c["createECDH"](…)`. Bracket access defeats the
191
+ // dotted-call regexes above (which require `name(`), so match a quoted method
192
+ // name inside a `[ '…' ]( ` computed call and route it to the right rule. The
193
+ // method names here take no key-type argument, so no argument parsing is needed;
194
+ // `generateKeyPair(Sync)` bracket access is handled separately (it carries the
195
+ // key-type literal). None of these overlap the dotted regexes (those require the
196
+ // name immediately before `(`, whereas here it is followed by a closing quote).
197
+ const RE_BRACKET_CRYPTO_METHOD =
198
+ /\[\s*['"`](createSign|createVerify|createECDH|createDiffieHellman|createDiffieHellmanGroup|publicEncrypt|privateDecrypt)['"`]\s*\]\s*\(/g;
199
+ // `crypto['generateKeyPairSync']('rsa', …)`: bracket access carrying the quoted
200
+ // key-type literal, classified exactly like the dotted `generateKeyPair` call.
201
+ const RE_BRACKET_KEYGEN =
202
+ /\[\s*['"`]generateKeyPair(?:Sync)?['"`]\s*\]\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
180
203
  // One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
181
204
  // consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
182
205
  // or `createSign(` (handled by the dedicated createSign/createVerify rule) —
@@ -375,6 +398,28 @@ const nodeCryptoDetector: Detector = {
375
398
  pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
376
399
  });
377
400
 
401
+ // generateKeyPair(Sync)(<variable>, …): key type passed as a variable, so the
402
+ // family is unknown; emit the generic keygen rule (unknown / key-exchange /
403
+ // HNDL-conservative) rather than missing the classical key generation.
404
+ eachMatch(RE_GENERATE_KEYPAIR_VAR, content, (m) => {
405
+ findings.push(
406
+ findingFromRule(
407
+ RULE_NODE_KEYGEN,
408
+ { file, content, index: m.index, matchLength: m[0].length },
409
+ {
410
+ title: "Classical key generation (variable key type)",
411
+ message:
412
+ "Generates a classical asymmetric key pair (key type passed as a variable), which is not quantum-safe.",
413
+ },
414
+ ),
415
+ );
416
+ });
417
+
418
+ // Bracket (computed-member) key generation: crypto['generateKeyPairSync']('rsa').
419
+ eachMatch(RE_BRACKET_KEYGEN, content, (m) => {
420
+ pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
421
+ });
422
+
378
423
  // Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
379
424
  // the CommonJS destructure-rename) so an aliased call still detects. Only the
380
425
  // keygen / ECDH / DH constructors are resolved — their classification is
@@ -432,6 +477,23 @@ const nodeCryptoDetector: Detector = {
432
477
  );
433
478
  });
434
479
 
480
+ // Bracket (computed-member) access to argument-free crypto methods:
481
+ // crypto['createSign'](…), c["createECDH"](…). Route each to its rule.
482
+ eachMatch(RE_BRACKET_CRYPTO_METHOD, content, (m) => {
483
+ const loc = { file, content, index: m.index, matchLength: m[0].length };
484
+ const method = m[1];
485
+ if (method === "createSign" || method === "createVerify") {
486
+ findings.push(findingFromRule(RULE_NODE_SIGN, loc));
487
+ } else if (method === "createECDH") {
488
+ findings.push(findingFromRule(RULE_NODE_ECDH, loc));
489
+ } else if (method === "publicEncrypt" || method === "privateDecrypt") {
490
+ findings.push(findingFromRule(RULE_NODE_RSA_ENCRYPT, loc));
491
+ } else {
492
+ // createDiffieHellman / createDiffieHellmanGroup: finite-field DH.
493
+ findings.push(findingFromRule(RULE_NODE_DH, loc));
494
+ }
495
+ });
496
+
435
497
  // One-shot crypto.sign(algorithm, data, key) / crypto.verify(...) (Node ≥ 12).
436
498
  eachMatch(RE_ONESHOT_SIGN_VERIFY, content, (m) => {
437
499
  findings.push(