@quantakrypto/core 0.4.3 → 0.4.4

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 (163) hide show
  1. package/dist/cbom-merge.d.ts +9 -0
  2. package/dist/cbom-merge.d.ts.map +1 -0
  3. package/dist/cbom-merge.js +99 -0
  4. package/dist/cbom-merge.js.map +1 -0
  5. package/dist/comments.d.ts +4 -3
  6. package/dist/comments.d.ts.map +1 -1
  7. package/dist/comments.js +53 -33
  8. package/dist/comments.js.map +1 -1
  9. package/dist/dependencies.d.ts.map +1 -1
  10. package/dist/dependencies.js +1 -1
  11. package/dist/dependencies.js.map +1 -1
  12. package/dist/detect-utils.d.ts +5 -1
  13. package/dist/detect-utils.d.ts.map +1 -1
  14. package/dist/detect-utils.js +11 -2
  15. package/dist/detect-utils.js.map +1 -1
  16. package/dist/detectors/c.d.ts +4 -2
  17. package/dist/detectors/c.d.ts.map +1 -1
  18. package/dist/detectors/c.js +192 -1
  19. package/dist/detectors/c.js.map +1 -1
  20. package/dist/detectors/cicd.d.ts +27 -0
  21. package/dist/detectors/cicd.d.ts.map +1 -0
  22. package/dist/detectors/cicd.js +116 -0
  23. package/dist/detectors/cicd.js.map +1 -0
  24. package/dist/detectors/cloud-kms.d.ts +21 -0
  25. package/dist/detectors/cloud-kms.d.ts.map +1 -0
  26. package/dist/detectors/cloud-kms.js +57 -0
  27. package/dist/detectors/cloud-kms.js.map +1 -0
  28. package/dist/detectors/cloudformation.d.ts +30 -0
  29. package/dist/detectors/cloudformation.d.ts.map +1 -0
  30. package/dist/detectors/cloudformation.js +170 -0
  31. package/dist/detectors/cloudformation.js.map +1 -0
  32. package/dist/detectors/database.d.ts +17 -0
  33. package/dist/detectors/database.d.ts.map +1 -0
  34. package/dist/detectors/database.js +60 -0
  35. package/dist/detectors/database.js.map +1 -0
  36. package/dist/detectors/dnssec.d.ts +55 -0
  37. package/dist/detectors/dnssec.d.ts.map +1 -0
  38. package/dist/detectors/dnssec.js +107 -0
  39. package/dist/detectors/dnssec.js.map +1 -0
  40. package/dist/detectors/elixir.d.ts +20 -0
  41. package/dist/detectors/elixir.d.ts.map +1 -0
  42. package/dist/detectors/elixir.js +195 -0
  43. package/dist/detectors/elixir.js.map +1 -0
  44. package/dist/detectors/java.d.ts.map +1 -1
  45. package/dist/detectors/java.js +1 -1
  46. package/dist/detectors/java.js.map +1 -1
  47. package/dist/detectors/jose.d.ts +20 -0
  48. package/dist/detectors/jose.d.ts.map +1 -0
  49. package/dist/detectors/jose.js +62 -0
  50. package/dist/detectors/jose.js.map +1 -0
  51. package/dist/detectors/jwk.d.ts +25 -0
  52. package/dist/detectors/jwk.d.ts.map +1 -0
  53. package/dist/detectors/jwk.js +95 -0
  54. package/dist/detectors/jwk.js.map +1 -0
  55. package/dist/detectors/k8s.d.ts +17 -0
  56. package/dist/detectors/k8s.d.ts.map +1 -0
  57. package/dist/detectors/k8s.js +85 -0
  58. package/dist/detectors/k8s.js.map +1 -0
  59. package/dist/detectors/mesh.d.ts +35 -0
  60. package/dist/detectors/mesh.d.ts.map +1 -0
  61. package/dist/detectors/mesh.js +109 -0
  62. package/dist/detectors/mesh.js.map +1 -0
  63. package/dist/detectors/messaging.d.ts +16 -0
  64. package/dist/detectors/messaging.d.ts.map +1 -0
  65. package/dist/detectors/messaging.js +72 -0
  66. package/dist/detectors/messaging.js.map +1 -0
  67. package/dist/detectors/pem.js +1 -1
  68. package/dist/detectors/pem.js.map +1 -1
  69. package/dist/detectors/php.d.ts +24 -0
  70. package/dist/detectors/php.d.ts.map +1 -0
  71. package/dist/detectors/php.js +167 -0
  72. package/dist/detectors/php.js.map +1 -0
  73. package/dist/detectors/python.d.ts.map +1 -1
  74. package/dist/detectors/python.js +70 -0
  75. package/dist/detectors/python.js.map +1 -1
  76. package/dist/detectors/rust.d.ts.map +1 -1
  77. package/dist/detectors/rust.js +68 -0
  78. package/dist/detectors/rust.js.map +1 -1
  79. package/dist/detectors/secrets.d.ts +21 -0
  80. package/dist/detectors/secrets.d.ts.map +1 -0
  81. package/dist/detectors/secrets.js +79 -0
  82. package/dist/detectors/secrets.js.map +1 -0
  83. package/dist/detectors/source.d.ts.map +1 -1
  84. package/dist/detectors/source.js +190 -47
  85. package/dist/detectors/source.js.map +1 -1
  86. package/dist/detectors/stateful-hbs.d.ts.map +1 -1
  87. package/dist/detectors/stateful-hbs.js +9 -6
  88. package/dist/detectors/stateful-hbs.js.map +1 -1
  89. package/dist/detectors/terraform.d.ts +21 -0
  90. package/dist/detectors/terraform.d.ts.map +1 -0
  91. package/dist/detectors/terraform.js +123 -0
  92. package/dist/detectors/terraform.js.map +1 -0
  93. package/dist/detectors/vpn.d.ts +22 -0
  94. package/dist/detectors/vpn.d.ts.map +1 -0
  95. package/dist/detectors/vpn.js +99 -0
  96. package/dist/detectors/vpn.js.map +1 -0
  97. package/dist/evidence.d.ts +5 -0
  98. package/dist/evidence.d.ts.map +1 -1
  99. package/dist/evidence.js +5 -0
  100. package/dist/evidence.js.map +1 -1
  101. package/dist/index.d.ts +5 -0
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +6 -0
  104. package/dist/index.js.map +1 -1
  105. package/dist/policy.d.ts +67 -0
  106. package/dist/policy.d.ts.map +1 -0
  107. package/dist/policy.js +115 -0
  108. package/dist/policy.js.map +1 -0
  109. package/dist/registry.d.ts.map +1 -1
  110. package/dist/registry.js +30 -0
  111. package/dist/registry.js.map +1 -1
  112. package/dist/remediate-pipeline.d.ts.map +1 -1
  113. package/dist/remediate-pipeline.js +22 -1
  114. package/dist/remediate-pipeline.js.map +1 -1
  115. package/dist/report.d.ts.map +1 -1
  116. package/dist/report.js +7 -0
  117. package/dist/report.js.map +1 -1
  118. package/dist/standards.d.ts +96 -0
  119. package/dist/standards.d.ts.map +1 -0
  120. package/dist/standards.js +107 -0
  121. package/dist/standards.js.map +1 -0
  122. package/dist/types.d.ts +1 -1
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/types.js.map +1 -1
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/dist/version.js.map +1 -1
  128. package/package.json +1 -1
  129. package/src/cbom-merge.ts +116 -0
  130. package/src/comments.ts +59 -33
  131. package/src/dependencies.ts +2 -1
  132. package/src/detect-utils.ts +14 -2
  133. package/src/detectors/c.ts +209 -3
  134. package/src/detectors/cicd.ts +158 -0
  135. package/src/detectors/cloud-kms.ts +87 -0
  136. package/src/detectors/cloudformation.ts +215 -0
  137. package/src/detectors/database.ts +88 -0
  138. package/src/detectors/dnssec.ts +186 -0
  139. package/src/detectors/elixir.ts +237 -0
  140. package/src/detectors/java.ts +2 -1
  141. package/src/detectors/jose.ts +94 -0
  142. package/src/detectors/jwk.ts +128 -0
  143. package/src/detectors/k8s.ts +112 -0
  144. package/src/detectors/mesh.ts +154 -0
  145. package/src/detectors/messaging.ts +98 -0
  146. package/src/detectors/pem.ts +1 -1
  147. package/src/detectors/php.ts +225 -0
  148. package/src/detectors/python.ts +71 -0
  149. package/src/detectors/rust.ts +71 -0
  150. package/src/detectors/secrets.ts +114 -0
  151. package/src/detectors/source.ts +235 -68
  152. package/src/detectors/stateful-hbs.ts +9 -6
  153. package/src/detectors/terraform.ts +154 -0
  154. package/src/detectors/vpn.ts +131 -0
  155. package/src/evidence.ts +11 -0
  156. package/src/index.ts +10 -0
  157. package/src/policy.ts +180 -0
  158. package/src/registry.ts +30 -0
  159. package/src/remediate-pipeline.ts +25 -2
  160. package/src/report.ts +7 -0
  161. package/src/standards.ts +186 -0
  162. package/src/types.ts +2 -0
  163. package/src/version.ts +1 -1
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Config detector: secrets encrypted at rest with classical asymmetric key
3
+ * wrapping. This is the purest "harvest now, decrypt later" surface — the
4
+ * ciphertext is often committed to a git repository, which means it is already
5
+ * replicated, effectively immortal, and retroactively un-fixable (you can
6
+ * re-encrypt HEAD, not history). Every recipient key here is classical.
7
+ *
8
+ * Covered:
9
+ * - Mozilla SOPS / age recipients: `age1…` bech32 public keys (X25519 key
10
+ * agreement wrapping the data key).
11
+ * - PGP-encrypted payloads: `-----BEGIN PGP MESSAGE-----` (RSA/ElGamal ESK).
12
+ * - Bitnami Sealed Secrets: `kind: SealedSecret` (controller wraps with RSA-OAEP).
13
+ *
14
+ * Symmetric-only schemes (ansible-vault AES, age with a scrypt passphrase) are
15
+ * intentionally NOT flagged: a strong symmetric key is only Grover-weakened, not
16
+ * broken, so it is out of scope for a *classical-asymmetric* readiness signal.
17
+ */
18
+ import type { Detector } from "../types.js";
19
+ /** Detects secrets encrypted at rest with classical asymmetric key wrapping. */
20
+ export declare const secretsDetector: Detector;
21
+ //# sourceMappingURL=secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/detectors/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAqB,MAAM,aAAa,CAAC;AAmE/D,gFAAgF;AAChF,eAAO,MAAM,eAAe,EAAE,QA4B7B,CAAC"}
@@ -0,0 +1,79 @@
1
+ import { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
2
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
3
+ const SECRET_RULES = [
4
+ {
5
+ // age/SOPS recipient: `age1` + 58 bech32 chars. Distinctive enough for any file.
6
+ re: /\bage1[0-9a-z]{58}\b/g,
7
+ meta: {
8
+ id: "secrets-age-recipient",
9
+ title: "age / SOPS recipient (X25519)",
10
+ description: "An age (SOPS) recipient public key wraps secrets with classical X25519",
11
+ category: "key-exchange",
12
+ severity: "high",
13
+ confidence: "high",
14
+ algorithm: "X25519",
15
+ hndl: true,
16
+ cwe: CWE_BROKEN_CRYPTO,
17
+ message: "Secrets are wrapped to an age/SOPS X25519 recipient (classical key agreement); harvest-now-decrypt-later exposed, and if committed to git the ciphertext is retroactively un-fixable.",
18
+ remediation: "Track a post-quantum age recipient / KMS (ML-KEM) and re-encrypt; rotate any secret whose ciphertext has left your control.",
19
+ },
20
+ },
21
+ {
22
+ re: /-----BEGIN PGP MESSAGE-----/g,
23
+ meta: {
24
+ id: "secrets-pgp-message",
25
+ title: "PGP-encrypted secret (RSA/ElGamal)",
26
+ description: "A PGP MESSAGE block: the session key is wrapped with classical RSA/ElGamal",
27
+ category: "kem",
28
+ severity: "high",
29
+ confidence: "high",
30
+ algorithm: "RSA",
31
+ hndl: true,
32
+ cwe: CWE_BROKEN_CRYPTO,
33
+ message: "A PGP-encrypted secret whose session key is wrapped with classical RSA/ElGamal; harvest-now-decrypt-later exposed.",
34
+ remediation: "Re-encrypt with a post-quantum KEM (ML-KEM-768) once available; rotate the underlying secret.",
35
+ },
36
+ },
37
+ {
38
+ re: /\bkind:\s*["']?SealedSecret\b/g,
39
+ meta: {
40
+ id: "secrets-sealed-secret",
41
+ title: "Bitnami Sealed Secret (RSA-OAEP)",
42
+ description: "A SealedSecret is wrapped by the controller's classical RSA-OAEP key",
43
+ category: "kem",
44
+ severity: "high",
45
+ confidence: "high",
46
+ algorithm: "RSA",
47
+ hndl: true,
48
+ cwe: CWE_BROKEN_CRYPTO,
49
+ message: "A Bitnami SealedSecret is wrapped with the controller's classical RSA-OAEP key; harvest-now-decrypt-later exposed, and typically committed to git.",
50
+ remediation: "Plan migration as sealed-secrets adds PQC support; rotate the sealing key and secrets when it does.",
51
+ },
52
+ },
53
+ ];
54
+ /** Detects secrets encrypted at rest with classical asymmetric key wrapping. */
55
+ export const secretsDetector = {
56
+ id: "secrets-at-rest",
57
+ description: "Secrets wrapped at rest with classical asymmetric crypto (SOPS/age, PGP, Sealed Secrets)",
58
+ scope: "config",
59
+ language: "any",
60
+ rules: SECRET_RULES.map((r) => r.meta),
61
+ // Skip prose/docs: a tutorial showing an example age recipient is not a secret store.
62
+ appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
63
+ detect({ file, content }) {
64
+ // Fast reject: none of the distinctive markers present.
65
+ if (!content.includes("age1") &&
66
+ !content.includes("BEGIN PGP MESSAGE") &&
67
+ !content.includes("SealedSecret")) {
68
+ return [];
69
+ }
70
+ const findings = [];
71
+ for (const rule of SECRET_RULES) {
72
+ eachMatch(rule.re, content, (m) => {
73
+ findings.push(findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }));
74
+ });
75
+ }
76
+ return findings;
77
+ },
78
+ };
79
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/detectors/secrets.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAO9C,MAAM,YAAY,GAAiB;IACjC;QACE,iFAAiF;QACjF,EAAE,EAAE,uBAAuB;QAC3B,IAAI,EAAE;YACJ,EAAE,EAAE,uBAAuB;YAC3B,KAAK,EAAE,+BAA+B;YACtC,WAAW,EAAE,wEAAwE;YACrF,QAAQ,EAAE,cAAc;YACxB,QAAQ,EAAE,MAAM;YAChB,UAAU,EAAE,MAAM;YAClB,SAAS,EAAE,QAAQ;YACnB,IAAI,EAAE,IAAI;YACV,GAAG,EAAE,iBAAiB;YACtB,OAAO,EACL,uLAAuL;YACzL,WAAW,EACT,6HAA6H;SAChI;KACF;IACD;QACE,EAAE,EAAE,8BAA8B;QAClC,IAAI,EAAE;YACJ,EAAE,EAAE,qBAAqB;YACzB,KAAK,EAAE,oCAAoC;YAC3C,WAAW,EAAE,4EAA4E;YACzF,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,MAAM;YAChB,UAAU,EAAE,MAAM;YAClB,SAAS,EAAE,KAAK;YAChB,IAAI,EAAE,IAAI;YACV,GAAG,EAAE,iBAAiB;YACtB,OAAO,EACL,oHAAoH;YACtH,WAAW,EACT,+FAA+F;SAClG;KACF;IACD;QACE,EAAE,EAAE,gCAAgC;QACpC,IAAI,EAAE;YACJ,EAAE,EAAE,uBAAuB;YAC3B,KAAK,EAAE,kCAAkC;YACzC,WAAW,EAAE,sEAAsE;YACnF,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,MAAM;YAChB,UAAU,EAAE,MAAM;YAClB,SAAS,EAAE,KAAK;YAChB,IAAI,EAAE,IAAI;YACV,GAAG,EAAE,iBAAiB;YACtB,OAAO,EACL,oJAAoJ;YACtJ,WAAW,EACT,qGAAqG;SACxG;KACF;CACF,CAAC;AAEF,gFAAgF;AAChF,MAAM,CAAC,MAAM,eAAe,GAAa;IACvC,EAAE,EAAE,iBAAiB;IACrB,WAAW,EACT,0FAA0F;IAC5F,KAAK,EAAE,QAAQ;IACf,QAAQ,EAAE,KAAK;IACf,KAAK,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACtC,sFAAsF;IACtF,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC,EAAE,cAAc,CAAC;IAClD,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE;QACtB,wDAAwD;QACxD,IACE,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YACzB,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC;YACtC,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EACjC,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;YAChC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;gBAChC,QAAQ,CAAC,IAAI,CACX,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CACxF,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC","sourcesContent":["/**\n * Config detector: secrets encrypted at rest with classical asymmetric key\n * wrapping. This is the purest \"harvest now, decrypt later\" surface — the\n * ciphertext is often committed to a git repository, which means it is already\n * replicated, effectively immortal, and retroactively un-fixable (you can\n * re-encrypt HEAD, not history). Every recipient key here is classical.\n *\n * Covered:\n * - Mozilla SOPS / age recipients: `age1…` bech32 public keys (X25519 key\n * agreement wrapping the data key).\n * - PGP-encrypted payloads: `-----BEGIN PGP MESSAGE-----` (RSA/ElGamal ESK).\n * - Bitnami Sealed Secrets: `kind: SealedSecret` (controller wraps with RSA-OAEP).\n *\n * Symmetric-only schemes (ansible-vault AES, age with a scrypt passphrase) are\n * intentionally NOT flagged: a strong symmetric key is only Grover-weakened, not\n * broken, so it is out of scope for a *classical-asymmetric* readiness signal.\n */\nimport type { Detector, Finding, RuleMeta } from \"../types.js\";\nimport { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from \"../detect-utils.js\";\nimport { CWE_BROKEN_CRYPTO } from \"../cwe.js\";\n\ninterface SecretRule {\n re: RegExp;\n meta: RuleMeta;\n}\n\nconst SECRET_RULES: SecretRule[] = [\n {\n // age/SOPS recipient: `age1` + 58 bech32 chars. Distinctive enough for any file.\n re: /\\bage1[0-9a-z]{58}\\b/g,\n meta: {\n id: \"secrets-age-recipient\",\n title: \"age / SOPS recipient (X25519)\",\n description: \"An age (SOPS) recipient public key wraps secrets with classical X25519\",\n category: \"key-exchange\",\n severity: \"high\",\n confidence: \"high\",\n algorithm: \"X25519\",\n hndl: true,\n cwe: CWE_BROKEN_CRYPTO,\n message:\n \"Secrets are wrapped to an age/SOPS X25519 recipient (classical key agreement); harvest-now-decrypt-later exposed, and if committed to git the ciphertext is retroactively un-fixable.\",\n remediation:\n \"Track a post-quantum age recipient / KMS (ML-KEM) and re-encrypt; rotate any secret whose ciphertext has left your control.\",\n },\n },\n {\n re: /-----BEGIN PGP MESSAGE-----/g,\n meta: {\n id: \"secrets-pgp-message\",\n title: \"PGP-encrypted secret (RSA/ElGamal)\",\n description: \"A PGP MESSAGE block: the session key is wrapped with classical RSA/ElGamal\",\n category: \"kem\",\n severity: \"high\",\n confidence: \"high\",\n algorithm: \"RSA\",\n hndl: true,\n cwe: CWE_BROKEN_CRYPTO,\n message:\n \"A PGP-encrypted secret whose session key is wrapped with classical RSA/ElGamal; harvest-now-decrypt-later exposed.\",\n remediation:\n \"Re-encrypt with a post-quantum KEM (ML-KEM-768) once available; rotate the underlying secret.\",\n },\n },\n {\n re: /\\bkind:\\s*[\"']?SealedSecret\\b/g,\n meta: {\n id: \"secrets-sealed-secret\",\n title: \"Bitnami Sealed Secret (RSA-OAEP)\",\n description: \"A SealedSecret is wrapped by the controller's classical RSA-OAEP key\",\n category: \"kem\",\n severity: \"high\",\n confidence: \"high\",\n algorithm: \"RSA\",\n hndl: true,\n cwe: CWE_BROKEN_CRYPTO,\n message:\n \"A Bitnami SealedSecret is wrapped with the controller's classical RSA-OAEP key; harvest-now-decrypt-later exposed, and typically committed to git.\",\n remediation:\n \"Plan migration as sealed-secrets adds PQC support; rotate the sealing key and secrets when it does.\",\n },\n },\n];\n\n/** Detects secrets encrypted at rest with classical asymmetric key wrapping. */\nexport const secretsDetector: Detector = {\n id: \"secrets-at-rest\",\n description:\n \"Secrets wrapped at rest with classical asymmetric crypto (SOPS/age, PGP, Sealed Secrets)\",\n scope: \"config\",\n language: \"any\",\n rules: SECRET_RULES.map((r) => r.meta),\n // Skip prose/docs: a tutorial showing an example age recipient is not a secret store.\n appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),\n detect({ file, content }): Finding[] {\n // Fast reject: none of the distinctive markers present.\n if (\n !content.includes(\"age1\") &&\n !content.includes(\"BEGIN PGP MESSAGE\") &&\n !content.includes(\"SealedSecret\")\n ) {\n return [];\n }\n const findings: Finding[] = [];\n for (const rule of SECRET_RULES) {\n eachMatch(rule.re, content, (m) => {\n findings.push(\n findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }),\n );\n });\n }\n return findings;\n },\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/detectors/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAqB,MAAM,aAAa,CAAC;AA4/B/D,0DAA0D;AAC1D,eAAO,MAAM,eAAe,EAAE,QAAQ,EAQrC,CAAC"}
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/detectors/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAqB,MAAM,aAAa,CAAC;AAmqC/D,0DAA0D;AAC1D,eAAO,MAAM,eAAe,EAAE,QAAQ,EAQrC,CAAC"}
@@ -6,6 +6,107 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
6
6
  // `rsa-pss` is listed before `rsa` so the alternation consumes the full token
7
7
  // (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
8
8
  const RE_GENERATE_KEYPAIR = /generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
9
+ const KEYGEN_INFO = {
10
+ rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
11
+ // RSA-PSS is signature-only, so classify it as a (forgeable) signature
12
+ // rather than a KEM — no HNDL confidentiality exposure.
13
+ "rsa-pss": {
14
+ algo: "RSA",
15
+ cat: "signature",
16
+ sev: "high",
17
+ hndl: false,
18
+ label: "RSA-PSS",
19
+ message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
20
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
21
+ },
22
+ // EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
23
+ // HNDL-exposed, so classify conservatively as key-exchange-capable and
24
+ // surface both concerns rather than asserting signature-only (P0-4).
25
+ ec: {
26
+ algo: "ECDH",
27
+ cat: "key-exchange",
28
+ sev: "high",
29
+ hndl: true,
30
+ label: "EC (ECDSA/ECDH)",
31
+ message: "Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
32
+ "and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
33
+ remediation: "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
34
+ },
35
+ dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
36
+ dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
37
+ x25519: { algo: "X25519", cat: "key-exchange", sev: "medium", hndl: true, label: "X25519" },
38
+ x448: { algo: "X448", cat: "key-exchange", sev: "medium", hndl: true, label: "X448" },
39
+ ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
40
+ ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
41
+ };
42
+ /** Emit a keygen finding for `generateKeyPair(Sync)('<type>')` at [index]. Shared
43
+ * by the direct matcher and the import-alias pass so both classify identically. */
44
+ function pushKeygenFinding(findings, rawType, file, content, index, matchLength) {
45
+ const info = KEYGEN_INFO[rawType.toLowerCase()];
46
+ if (!info)
47
+ return;
48
+ findings.push(findingFromRule(RULE_NODE_KEYGEN, { file, content, index, matchLength }, {
49
+ title: `${info.label} key generation`,
50
+ category: info.cat,
51
+ severity: info.sev,
52
+ algorithm: info.algo,
53
+ hndl: info.hndl,
54
+ message: info.message ??
55
+ `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
56
+ ...(info.remediation ? { remediation: info.remediation } : {}),
57
+ }));
58
+ }
59
+ /** Canonical Node-crypto call names whose import-aliases we resolve. */
60
+ const ALIASABLE = [
61
+ "generateKeyPairSync",
62
+ "generateKeyPair",
63
+ "createECDH",
64
+ "createDiffieHellman",
65
+ "createDiffieHellmanGroup",
66
+ ];
67
+ /**
68
+ * Collect import-aliases for the canonical crypto calls we resolve, so
69
+ * `import { generateKeyPairSync as gk } from 'node:crypto'` (or the CommonJS
70
+ * `const { generateKeyPairSync: gk } = require('crypto')`) is followed when `gk`
71
+ * is called later. Returns Map<canonical, alias[]>. An alias equal to its own
72
+ * canonical name is skipped (already matched directly). Precision-safe: the
73
+ * alias is only ever bound to a known crypto symbol, so a later `gk(` call is
74
+ * unambiguously that symbol.
75
+ */
76
+ function collectCryptoAliases(content) {
77
+ const out = new Map();
78
+ const add = (canonical, alias) => {
79
+ if (!alias || alias === canonical)
80
+ return;
81
+ const list = out.get(canonical) ?? [];
82
+ if (!list.includes(alias))
83
+ list.push(alias);
84
+ out.set(canonical, list);
85
+ };
86
+ // ESM named imports: `import { A as B, C as D } from '...'`.
87
+ const esm = /import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"][^'"]*['"]/g;
88
+ for (let m = esm.exec(content); m; m = esm.exec(content)) {
89
+ const spec = /([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/g;
90
+ for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
91
+ if (ALIASABLE.includes(s[1]))
92
+ add(s[1], s[2]);
93
+ }
94
+ }
95
+ // CommonJS destructure-rename: `const { A: B } = require('...')`.
96
+ const cjs = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['"][^'"]*['"]\s*\)/g;
97
+ for (let m = cjs.exec(content); m; m = cjs.exec(content)) {
98
+ const spec = /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)/g;
99
+ for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
100
+ if (ALIASABLE.includes(s[1]))
101
+ add(s[1], s[2]);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ /** Escape a string for safe interpolation into a dynamically-built RegExp. */
107
+ function escapeRe(s) {
108
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
109
+ }
9
110
  const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
10
111
  // One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
11
112
  // consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
@@ -35,7 +136,7 @@ const RE_JSRSASIGN_KEYGEN = /KEYUTIL\.generateKeypair\s*\(/g;
35
136
  const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
36
137
  const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
37
138
  // secp256k1 — direct @noble/secp256k1 / secp256k1-style API usage in source.
38
- const RE_SECP256K1 = /\b(?:secp(?:256k1)?|secp)\s*\.\s*(?:sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
139
+ const RE_SECP256K1 = /\b(?:secp(?:256k1)?|secp)\s*\.\s*(sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
39
140
  // JWT/JOSE.
40
141
  const RE_JWT_ALG = /['"`](RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512|256K)|EdDSA)['"`]/g;
41
142
  // JOSE ECDH-ES key agreement (HNDL) and COSE algorithm identifiers.
@@ -177,52 +278,41 @@ const nodeCryptoDetector = {
177
278
  const findings = [];
178
279
  // generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
179
280
  eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
180
- const type = m[1].toLowerCase();
181
- const map = {
182
- rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
183
- // RSA-PSS is signature-only, so classify it as a (forgeable) signature
184
- // rather than a KEM — no HNDL confidentiality exposure.
185
- "rsa-pss": {
186
- algo: "RSA",
187
- cat: "signature",
188
- sev: "high",
189
- hndl: false,
190
- label: "RSA-PSS",
191
- message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
192
- remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
193
- },
194
- // EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
195
- // HNDL-exposed, so classify conservatively as key-exchange-capable and
196
- // surface both concerns rather than asserting signature-only (P0-4).
197
- ec: {
198
- algo: "ECDH",
199
- cat: "key-exchange",
200
- sev: "high",
201
- hndl: true,
202
- label: "EC (ECDSA/ECDH)",
203
- message: "Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
204
- "and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
205
- remediation: "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
206
- },
207
- dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
208
- dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
209
- x25519: { algo: "X25519", cat: "key-exchange", sev: "medium", hndl: true, label: "X25519" },
210
- x448: { algo: "X448", cat: "key-exchange", sev: "medium", hndl: true, label: "X448" },
211
- ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
212
- ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
213
- };
214
- const info = map[type];
215
- findings.push(findingFromRule(RULE_NODE_KEYGEN, { file, content, index: m.index, matchLength: m[0].length }, {
216
- title: `${info.label} key generation`,
217
- category: info.cat,
218
- severity: info.sev,
219
- algorithm: info.algo,
220
- hndl: info.hndl,
221
- message: info.message ??
222
- `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
223
- ...(info.remediation ? { remediation: info.remediation } : {}),
224
- }));
281
+ pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
225
282
  });
283
+ // Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
284
+ // the CommonJS destructure-rename) so an aliased call still detects. Only the
285
+ // keygen / ECDH / DH constructors are resolved — their classification is
286
+ // unambiguous. The alias regexes run on the ORIGINAL content, so locations
287
+ // stay exact; it fires only when the alias was explicitly bound to a known
288
+ // crypto symbol, so precision is unaffected.
289
+ const aliases = collectCryptoAliases(content);
290
+ for (const [canonical, names] of aliases) {
291
+ for (const alias of names) {
292
+ const a = escapeRe(alias);
293
+ if (canonical === "generateKeyPairSync" || canonical === "generateKeyPair") {
294
+ const re = new RegExp(`\\b${a}\\s*\\(\\s*['"\`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"\`]`, "g");
295
+ eachMatch(re, content, (m) => pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length));
296
+ }
297
+ else if (canonical === "createECDH") {
298
+ eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_ECDH, {
299
+ file,
300
+ content,
301
+ index: m.index,
302
+ matchLength: m[0].length,
303
+ })));
304
+ }
305
+ else {
306
+ // createDiffieHellman / createDiffieHellmanGroup.
307
+ eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_DH, {
308
+ file,
309
+ content,
310
+ index: m.index,
311
+ matchLength: m[0].length,
312
+ })));
313
+ }
314
+ }
315
+ }
226
316
  // createSign / createVerify — RSA / ECDSA / DSA signatures.
227
317
  eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
228
318
  findings.push(findingFromRule(RULE_NODE_SIGN, {
@@ -485,7 +575,21 @@ const libraryDetector = {
485
575
  add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
486
576
  add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
487
577
  add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
488
- add(RE_SECP256K1, RULE_SECP256K1); // secp.sign / getPublicKey / getSharedSecret
578
+ // secp256k1: classify per method getSharedSecret/ecdh are key AGREEMENT
579
+ // (ECDH, harvest-now-decrypt-later); sign/verify/getPublicKey/recover are
580
+ // ECDSA signatures (not HNDL). The old rule flagged everything as ECDSA.
581
+ eachMatch(RE_SECP256K1, content, (m) => {
582
+ const kex = m[1] === "getSharedSecret" || m[1] === "ecdh";
583
+ findings.push(findingFromRule(RULE_SECP256K1, { file, content, index: m.index, matchLength: m[0].length }, kex
584
+ ? {
585
+ title: "secp256k1 ECDH key agreement",
586
+ category: "key-exchange",
587
+ algorithm: "ECDH",
588
+ hndl: true,
589
+ message: `secp256k1 ECDH key agreement (.${m[1]}()) is classical and harvest-now-decrypt-later exposed.`,
590
+ }
591
+ : undefined));
592
+ });
489
593
  add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
490
594
  add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
491
595
  add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
@@ -678,6 +782,41 @@ const RE_CERT_SIG_ALG = /\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA
678
782
  // Shor-broken key agreement — the harvest-now-decrypt-later surface a lexical
679
783
  // scan misses when only the `crypto/*` API is matched, not the negotiated kex.
680
784
  const RE_SSH_KEX = /\b(diffie-hellman-group(?:1|14|15|16|17|18)(?:-sha1|-sha256|-sha512)?|diffie-hellman-group-exchange-sha(?:1|256)|ecdh-sha2-nistp(?:256|384|521)|curve25519-sha256)\b/g;
785
+ // A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
786
+ // when it is EITHER (a) followed by base64 key material — an actual
787
+ // authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
788
+ // algorithm tokens on the same line — an algorithm-preference list
789
+ // (`HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256,ssh-rsa`, `%w[…]`, …). A
790
+ // lone token, e.g. a UI label / i18n value like `"ssh-rsa": "ssh-rsa"`, is
791
+ // neither, so it must NOT be flagged. `RE_SSH_ALGO_TOKEN` is a separate (cloned-
792
+ // on-use) copy so counting per line never disturbs the outer `eachMatch` state.
793
+ const RE_SSH_ALGO_TOKEN = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
794
+ /** How far around a token we look for a sibling algorithm on the same line. A
795
+ * real `HostKeyAlgorithms` list is short; bounding the window keeps the per-token
796
+ * work O(1) so a pathological single huge line can't make detection quadratic. */
797
+ const SSH_LINE_WINDOW = 512;
798
+ function isRealSshKeyOrAlgoList(content, index, matchLen) {
799
+ // (a) base64 key material immediately after the token → a real SSH public key.
800
+ if (/^\s+[A-Za-z0-9+/]{20,}/.test(content.slice(index + matchLen, index + matchLen + 80))) {
801
+ return true;
802
+ }
803
+ // (b) ≥2 distinct ssh key/host-key algorithm tokens on the same line → a list.
804
+ // Bound both the back-scan for the line start and the window itself so the work
805
+ // is constant per token and never crosses a line boundary (i18n entries live on
806
+ // separate lines, so a lone repeated token stays a single distinct token).
807
+ const from = Math.max(0, index - SSH_LINE_WINDOW);
808
+ const pre = content.slice(from, index);
809
+ const preNl = pre.lastIndexOf("\n");
810
+ const lineStart = preNl === -1 ? from : from + preNl + 1;
811
+ const to = Math.min(content.length, index + matchLen + SSH_LINE_WINDOW);
812
+ const post = content.slice(index, to);
813
+ const postNl = post.indexOf("\n");
814
+ const lineEnd = postNl === -1 ? to : index + postNl;
815
+ const distinct = new Set();
816
+ for (const t of content.slice(lineStart, lineEnd).matchAll(RE_SSH_ALGO_TOKEN))
817
+ distinct.add(t[1]);
818
+ return distinct.size >= 2;
819
+ }
681
820
  const RULE_SSH_PUBKEY = {
682
821
  id: "ssh-public-key",
683
822
  title: "Classical SSH public key",
@@ -737,6 +876,10 @@ const sshCertDetector = {
737
876
  const findings = [];
738
877
  // SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
739
878
  eachMatch(RE_SSH_PUBKEY, content, (m) => {
879
+ // Skip a bare token that is neither a real key nor an algorithm list (e.g.
880
+ // a `"ssh-rsa": "ssh-rsa"` i18n label) — see `isRealSshKeyOrAlgoList`.
881
+ if (!isRealSshKeyOrAlgoList(content, m.index, m[0].length))
882
+ return;
740
883
  const tok = m[1];
741
884
  const algorithm = tok.startsWith("ssh-rsa")
742
885
  ? "RSA"