@quantakrypto/core 0.4.2 → 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 (132) 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.js.map +1 -1
  7. package/dist/changed.js.map +1 -1
  8. package/dist/codemods/config-toggle.js.map +1 -1
  9. package/dist/codemods/registry.js.map +1 -1
  10. package/dist/comments.d.ts +26 -0
  11. package/dist/comments.d.ts.map +1 -1
  12. package/dist/comments.js +141 -1
  13. package/dist/comments.js.map +1 -1
  14. package/dist/config.js.map +1 -1
  15. package/dist/cwe.js.map +1 -1
  16. package/dist/dependencies.d.ts +3 -2
  17. package/dist/dependencies.d.ts.map +1 -1
  18. package/dist/dependencies.js +138 -2
  19. package/dist/dependencies.js.map +1 -1
  20. package/dist/detect-utils.d.ts +14 -3
  21. package/dist/detect-utils.d.ts.map +1 -1
  22. package/dist/detect-utils.js +32 -4
  23. package/dist/detect-utils.js.map +1 -1
  24. package/dist/detectors/c.d.ts.map +1 -1
  25. package/dist/detectors/c.js +190 -2
  26. package/dist/detectors/c.js.map +1 -1
  27. package/dist/detectors/csharp.d.ts.map +1 -1
  28. package/dist/detectors/csharp.js +141 -3
  29. package/dist/detectors/csharp.js.map +1 -1
  30. package/dist/detectors/go.d.ts +5 -3
  31. package/dist/detectors/go.d.ts.map +1 -1
  32. package/dist/detectors/go.js +187 -3
  33. package/dist/detectors/go.js.map +1 -1
  34. package/dist/detectors/java.d.ts.map +1 -1
  35. package/dist/detectors/java.js +168 -3
  36. package/dist/detectors/java.js.map +1 -1
  37. package/dist/detectors/pem.d.ts.map +1 -1
  38. package/dist/detectors/pem.js +49 -1
  39. package/dist/detectors/pem.js.map +1 -1
  40. package/dist/detectors/python.d.ts.map +1 -1
  41. package/dist/detectors/python.js +74 -3
  42. package/dist/detectors/python.js.map +1 -1
  43. package/dist/detectors/ruby.d.ts +9 -5
  44. package/dist/detectors/ruby.d.ts.map +1 -1
  45. package/dist/detectors/ruby.js +122 -2
  46. package/dist/detectors/ruby.js.map +1 -1
  47. package/dist/detectors/rust.d.ts +8 -3
  48. package/dist/detectors/rust.d.ts.map +1 -1
  49. package/dist/detectors/rust.js +185 -3
  50. package/dist/detectors/rust.js.map +1 -1
  51. package/dist/detectors/source.d.ts.map +1 -1
  52. package/dist/detectors/source.js +117 -7
  53. package/dist/detectors/source.js.map +1 -1
  54. package/dist/detectors/stateful-hbs.d.ts +22 -0
  55. package/dist/detectors/stateful-hbs.d.ts.map +1 -0
  56. package/dist/detectors/stateful-hbs.js +139 -0
  57. package/dist/detectors/stateful-hbs.js.map +1 -0
  58. package/dist/errors.js.map +1 -1
  59. package/dist/evidence.d.ts +48 -0
  60. package/dist/evidence.d.ts.map +1 -0
  61. package/dist/evidence.js +76 -0
  62. package/dist/evidence.js.map +1 -0
  63. package/dist/index.d.ts +4 -2
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +4 -2
  66. package/dist/index.js.map +1 -1
  67. package/dist/inventory.d.ts +2 -0
  68. package/dist/inventory.d.ts.map +1 -1
  69. package/dist/inventory.js +27 -1
  70. package/dist/inventory.js.map +1 -1
  71. package/dist/parallel.js.map +1 -1
  72. package/dist/patch-policy.js.map +1 -1
  73. package/dist/redact.d.ts.map +1 -1
  74. package/dist/redact.js +39 -0
  75. package/dist/redact.js.map +1 -1
  76. package/dist/registry.d.ts +2 -2
  77. package/dist/registry.d.ts.map +1 -1
  78. package/dist/registry.js +4 -2
  79. package/dist/registry.js.map +1 -1
  80. package/dist/remediate-pipeline.d.ts +3 -0
  81. package/dist/remediate-pipeline.d.ts.map +1 -1
  82. package/dist/remediate-pipeline.js +50 -0
  83. package/dist/remediate-pipeline.js.map +1 -1
  84. package/dist/remediate-request.js.map +1 -1
  85. package/dist/remediation.d.ts +9 -3
  86. package/dist/remediation.d.ts.map +1 -1
  87. package/dist/remediation.js +17 -5
  88. package/dist/remediation.js.map +1 -1
  89. package/dist/report.d.ts +9 -0
  90. package/dist/report.d.ts.map +1 -1
  91. package/dist/report.js +31 -0
  92. package/dist/report.js.map +1 -1
  93. package/dist/scan-worker.js.map +1 -1
  94. package/dist/scan.d.ts.map +1 -1
  95. package/dist/scan.js +8 -1
  96. package/dist/scan.js.map +1 -1
  97. package/dist/severity.js.map +1 -1
  98. package/dist/triage.js.map +1 -1
  99. package/dist/types.d.ts +1 -1
  100. package/dist/types.d.ts.map +1 -1
  101. package/dist/types.js.map +1 -1
  102. package/dist/verify.js.map +1 -1
  103. package/dist/version.d.ts +1 -1
  104. package/dist/version.js +1 -1
  105. package/dist/version.js.map +1 -1
  106. package/dist/walk.js.map +1 -1
  107. package/dist/worktree.js.map +1 -1
  108. package/package.json +1 -1
  109. package/src/comments.ts +140 -1
  110. package/src/dependencies.ts +143 -4
  111. package/src/detect-utils.ts +33 -4
  112. package/src/detectors/c.ts +202 -2
  113. package/src/detectors/csharp.ts +154 -3
  114. package/src/detectors/go.ts +209 -6
  115. package/src/detectors/java.ts +189 -3
  116. package/src/detectors/pem.ts +53 -1
  117. package/src/detectors/python.ts +82 -3
  118. package/src/detectors/ruby.ts +138 -7
  119. package/src/detectors/rust.ts +211 -7
  120. package/src/detectors/source.ts +149 -6
  121. package/src/detectors/stateful-hbs.ts +177 -0
  122. package/src/evidence.ts +124 -0
  123. package/src/index.ts +5 -1
  124. package/src/inventory.ts +31 -1
  125. package/src/redact.ts +41 -0
  126. package/src/registry.ts +4 -2
  127. package/src/remediate-pipeline.ts +55 -0
  128. package/src/remediation.ts +23 -8
  129. package/src/report.ts +41 -2
  130. package/src/scan.ts +13 -1
  131. package/src/types.ts +1 -1
  132. package/src/version.ts +1 -1
package/src/comments.ts CHANGED
@@ -109,6 +109,54 @@ export function commentSpans(content: string, style: CommentStyle): Array<[numbe
109
109
  return spans;
110
110
  }
111
111
 
112
+ /**
113
+ * Compute Python triple-quoted string spans (`"""…"""` / `'''…'''`). These are
114
+ * docstrings / prose in practice, so a crypto *name* mentioned inside one
115
+ * (`:param key_type: eg "ssh-ed25519"`) should not fire a token finding — but a
116
+ * PEM key pasted into one is still real material, so {@link stripCommentFindings}
117
+ * exempts `pem-*` rules. Comments and normal strings are skipped so a `"""`
118
+ * delimiter inside them is not mis-detected.
119
+ */
120
+ export function pythonDocstringSpans(content: string): Array<[number, number]> {
121
+ const spans: Array<[number, number]> = [];
122
+ const n = content.length;
123
+ let i = 0;
124
+ while (i < n) {
125
+ const c = content[i];
126
+ if (c === "#") {
127
+ i++;
128
+ while (i < n && content[i] !== "\n") i++;
129
+ continue;
130
+ }
131
+ if ((c === '"' || c === "'") && content[i + 1] === c && content[i + 2] === c) {
132
+ const start = i;
133
+ i += 3;
134
+ while (i < n && !(content[i] === c && content[i + 1] === c && content[i + 2] === c)) i++;
135
+ i = Math.min(n, i + 3);
136
+ spans.push([start, i]);
137
+ continue;
138
+ }
139
+ if (c === '"' || c === "'") {
140
+ const q = c;
141
+ i++;
142
+ while (i < n) {
143
+ if (content[i] === "\\") {
144
+ i += 2;
145
+ continue;
146
+ }
147
+ if (content[i] === q) {
148
+ i++;
149
+ break;
150
+ }
151
+ i++;
152
+ }
153
+ continue;
154
+ }
155
+ i++;
156
+ }
157
+ return spans;
158
+ }
159
+
112
160
  /** True if `offset` falls inside one of the (sorted, non-overlapping) spans. */
113
161
  export function offsetInSpans(spans: ReadonlyArray<[number, number]>, offset: number): boolean {
114
162
  let lo = 0;
@@ -163,7 +211,9 @@ export function stripCommentFindings(
163
211
  const style = commentStyleForFile(file);
164
212
  if (!style) return findings;
165
213
  const spans = commentSpans(content, style);
166
- if (spans.length === 0) return findings;
214
+ // Python docstrings suppress prose *token* rules but keep real PEM material.
215
+ const docSpans = style === "hash" ? pythonDocstringSpans(content) : [];
216
+ if (spans.length === 0 && docSpans.length === 0) return findings;
167
217
 
168
218
  // 1-based line → start offset, to turn a finding's (line, column) back into an
169
219
  // absolute offset (column is 1-based: offset = lineStart + column - 1).
@@ -173,6 +223,95 @@ export function stripCommentFindings(
173
223
  }
174
224
 
175
225
  return findings.filter((f) => {
226
+ const start = lineStarts[f.location.line - 1] ?? 0;
227
+ const offset = start + ((f.location.column ?? 1) - 1);
228
+ if (offsetInSpans(spans, offset)) return false;
229
+ if (docSpans.length > 0 && !f.ruleId.startsWith("pem-") && offsetInSpans(docSpans, offset)) {
230
+ return false;
231
+ }
232
+ return true;
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Compute the string-literal spans (`[start, end)`) of `content`, skipping over
238
+ * comments so a quote inside a comment is not treated as a string. Used to
239
+ * suppress findings from IDENTIFIER-only rules (e.g. a Go `SigningMethodRS256`
240
+ * mentioned inside an error-message string) — the mirror of {@link commentSpans}.
241
+ */
242
+ export function stringSpans(content: string, style: CommentStyle): Array<[number, number]> {
243
+ const spans: Array<[number, number]> = [];
244
+ const n = content.length;
245
+ let i = 0;
246
+ while (i < n) {
247
+ const c = content[i];
248
+ if (style === "c" && c === "/" && content[i + 1] === "/") {
249
+ i += 2;
250
+ while (i < n && content[i] !== "\n") i++;
251
+ continue;
252
+ }
253
+ if (style === "c" && c === "/" && content[i + 1] === "*") {
254
+ i += 2;
255
+ while (i < n && !(content[i] === "*" && content[i + 1] === "/")) i++;
256
+ i = Math.min(n, i + 2);
257
+ continue;
258
+ }
259
+ if (style === "hash" && c === "#") {
260
+ i++;
261
+ while (i < n && content[i] !== "\n") i++;
262
+ continue;
263
+ }
264
+ if (c === '"' || c === "'" || c === "`") {
265
+ const quote = c;
266
+ const start = i;
267
+ i++;
268
+ while (i < n) {
269
+ if (content[i] === "\\") {
270
+ i += 2;
271
+ continue;
272
+ }
273
+ if (content[i] === quote) {
274
+ i++;
275
+ break;
276
+ }
277
+ i++;
278
+ }
279
+ spans.push([start, i]);
280
+ continue;
281
+ }
282
+ i++;
283
+ }
284
+ return spans;
285
+ }
286
+
287
+ /**
288
+ * Drop findings of "code-only" rules (`ruleIds`) whose match starts inside a
289
+ * string literal. Some rules — an identifier-form JWT signing method, a Go
290
+ * `SigningMethodRS256` — are only meaningful as code; when the same token appears
291
+ * inside a string (a test's `t.Error("SigningMethodPS256 …")`) it is prose, not a
292
+ * usage. Rules that legitimately match inside strings (quoted `"RS256"` alg
293
+ * tokens, cipher-suite strings, ssh-key tokens) are NOT in `ruleIds` and pass
294
+ * through untouched.
295
+ */
296
+ export function stripStringLiteralFindings(
297
+ findings: Finding[],
298
+ content: string,
299
+ file: string,
300
+ ruleIds: ReadonlySet<string>,
301
+ ): Finding[] {
302
+ if (findings.length === 0 || !findings.some((f) => ruleIds.has(f.ruleId))) return findings;
303
+ const style = commentStyleForFile(file);
304
+ if (!style) return findings;
305
+ const spans = stringSpans(content, style);
306
+ if (spans.length === 0) return findings;
307
+
308
+ const lineStarts: number[] = [0];
309
+ for (let i = 0; i < content.length; i++) {
310
+ if (content[i] === "\n") lineStarts.push(i + 1);
311
+ }
312
+
313
+ return findings.filter((f) => {
314
+ if (!ruleIds.has(f.ruleId)) return true;
176
315
  const start = lineStarts[f.location.line - 1] ?? 0;
177
316
  const offset = start + ((f.location.column ?? 1) - 1);
178
317
  return !offsetInSpans(spans, offset);
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Curated database of packages that primarily expose classical asymmetric
3
3
  * cryptography (and are therefore quantum-vulnerable), across multiple
4
- * ecosystems (npm, PyPI, crates.io, Go modules, Maven, RubyGems), plus a
4
+ * ecosystems (npm, PyPI, crates.io, Go modules, Maven, RubyGems, NuGet), plus a
5
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, …).
6
+ * (package.json, requirements.txt, Cargo.toml, go.mod, pom.xml, Gemfile,
7
+ * packages.config / *.csproj, …).
7
8
  *
8
9
  * The list is intentionally focused on libraries whose *purpose* is classical
9
10
  * public-key crypto. General-purpose packages that merely call the language's
@@ -313,6 +314,28 @@ export const vulnerableDependencies: VulnerableDependency[] = [
313
314
  algorithms: ["RSA", "ECDSA", "DSA"],
314
315
  severity: "high",
315
316
  },
317
+ {
318
+ name: "pycrypto",
319
+ ecosystem: "pypi",
320
+ reason: "Abandoned/unmaintained crypto library — classical RSA/DSA/ElGamal.",
321
+ algorithms: ["RSA", "DSA"],
322
+ severity: "high",
323
+ },
324
+ {
325
+ name: "jwcrypto",
326
+ ecosystem: "pypi",
327
+ reason: "JWK / JWS / JWE with classical RS*/ES* and ECDH-ES key agreement.",
328
+ algorithms: ["RSA", "ECDSA", "ECDH"],
329
+ severity: "medium",
330
+ },
331
+ {
332
+ name: "authlib",
333
+ ecosystem: "pypi",
334
+ reason: "OAuth / OIDC / JOSE stack using classical RS*/ES* JWT signing.",
335
+ algorithms: ["RSA", "ECDSA"],
336
+ severity: "medium",
337
+ hndl: false,
338
+ },
316
339
  {
317
340
  name: "pycryptodomex",
318
341
  ecosystem: "pypi",
@@ -422,6 +445,13 @@ export const vulnerableDependencies: VulnerableDependency[] = [
422
445
  algorithms: ["ECDSA", "ECDH"],
423
446
  severity: "high",
424
447
  },
448
+ {
449
+ name: "secp256k1",
450
+ ecosystem: "cargo",
451
+ reason: "libsecp256k1 bindings (blockchain) — classical ECDSA + ECDH.",
452
+ algorithms: ["ECDSA", "ECDH"],
453
+ severity: "high",
454
+ },
425
455
  {
426
456
  name: "ed25519-dalek",
427
457
  ecosystem: "cargo",
@@ -445,6 +475,43 @@ export const vulnerableDependencies: VulnerableDependency[] = [
445
475
  algorithms: ["RSA", "ECDSA", "EdDSA", "ECDH"],
446
476
  severity: "medium",
447
477
  },
478
+ {
479
+ name: "github.com/golang-jwt/jwt/v5",
480
+ ecosystem: "go",
481
+ reason: "golang-jwt — classical RS*/PS*/ES* JWT algorithms.",
482
+ algorithms: ["RSA", "ECDSA"],
483
+ severity: "medium",
484
+ hndl: false,
485
+ },
486
+ {
487
+ name: "github.com/golang-jwt/jwt/v4",
488
+ ecosystem: "go",
489
+ reason: "golang-jwt (v4) — classical RS*/PS*/ES* JWT algorithms.",
490
+ algorithms: ["RSA", "ECDSA"],
491
+ severity: "medium",
492
+ hndl: false,
493
+ },
494
+ {
495
+ name: "github.com/go-jose/go-jose/v4",
496
+ ecosystem: "go",
497
+ reason: "go-jose — classical JOSE (RSA/ECDSA signatures, ECDH-ES key agreement).",
498
+ algorithms: ["RSA", "ECDSA", "ECDH"],
499
+ severity: "medium",
500
+ },
501
+ {
502
+ name: "github.com/cloudflare/circl",
503
+ ecosystem: "go",
504
+ reason: "Cloudflare CIRCL — classical ECDH/EdDSA curves (X25519, X448, Ed25519, P-256).",
505
+ algorithms: ["ECDH", "EdDSA"],
506
+ severity: "medium",
507
+ },
508
+ {
509
+ name: "github.com/decred/dcrd/dcrec/secp256k1/v4",
510
+ ecosystem: "go",
511
+ reason: "decred secp256k1 — classical ECDSA/ECDH on the secp256k1 curve (blockchain keys).",
512
+ algorithms: ["ECDSA", "ECDH"],
513
+ severity: "medium",
514
+ },
448
515
 
449
516
  // --- Maven (Java) ---
450
517
  {
@@ -476,6 +543,21 @@ export const vulnerableDependencies: VulnerableDependency[] = [
476
543
  severity: "medium",
477
544
  hndl: false,
478
545
  },
546
+ {
547
+ name: "nimbus-jose-jwt",
548
+ ecosystem: "maven",
549
+ reason: "Nimbus JOSE+JWT — classical RS*/PS*/ES* JWS and RSA/ECDH-ES JWE.",
550
+ algorithms: ["RSA", "ECDSA", "ECDH"],
551
+ severity: "medium",
552
+ },
553
+ {
554
+ name: "jjwt-api",
555
+ ecosystem: "maven",
556
+ reason: "JJWT (io.jsonwebtoken) — classical RS*/ES* JWT signing.",
557
+ algorithms: ["RSA", "ECDSA"],
558
+ severity: "medium",
559
+ hndl: false,
560
+ },
479
561
 
480
562
  // --- RubyGems ---
481
563
  {
@@ -486,6 +568,14 @@ export const vulnerableDependencies: VulnerableDependency[] = [
486
568
  severity: "medium",
487
569
  hndl: false,
488
570
  },
571
+ {
572
+ name: "net-ssh",
573
+ ecosystem: "rubygems",
574
+ reason:
575
+ "Ruby SSH client — classical host/user keys (RSA/ECDSA/Ed25519) and ECDH/DH key exchange.",
576
+ algorithms: ["RSA", "ECDSA", "ECDH"],
577
+ severity: "medium",
578
+ },
489
579
  {
490
580
  name: "rbnacl",
491
581
  ecosystem: "rubygems",
@@ -500,13 +590,46 @@ export const vulnerableDependencies: VulnerableDependency[] = [
500
590
  algorithms: ["EdDSA"],
501
591
  severity: "low",
502
592
  },
593
+
594
+ // --- NuGet (.NET) ---
595
+ {
596
+ name: "BouncyCastle.Cryptography",
597
+ ecosystem: "nuget",
598
+ reason: "BouncyCastle for .NET — full classical RSA/ECDSA/ECDH/DSA suite.",
599
+ algorithms: ["RSA", "ECDSA", "ECDH", "DSA"],
600
+ severity: "high",
601
+ },
602
+ {
603
+ name: "Portable.BouncyCastle",
604
+ ecosystem: "nuget",
605
+ reason: "Portable BouncyCastle for .NET — classical RSA/ECDSA/ECDH/DSA.",
606
+ algorithms: ["RSA", "ECDSA", "ECDH", "DSA"],
607
+ severity: "high",
608
+ },
609
+ {
610
+ name: "System.IdentityModel.Tokens.Jwt",
611
+ ecosystem: "nuget",
612
+ reason: "Microsoft JWT handler — classical RS*/ES* (RSA/ECDSA) signatures.",
613
+ algorithms: ["RSA", "ECDSA"],
614
+ severity: "medium",
615
+ hndl: false,
616
+ },
617
+ {
618
+ name: "Microsoft.IdentityModel.Tokens",
619
+ ecosystem: "nuget",
620
+ reason: "Microsoft IdentityModel token crypto — classical RSA/ECDSA keys.",
621
+ algorithms: ["RSA", "ECDSA"],
622
+ severity: "medium",
623
+ hndl: false,
624
+ },
503
625
  ];
504
626
 
505
627
  /**
506
628
  * Normalise a package name for matching within its ecosystem. PyPI is
507
629
  * case-insensitive and folds runs of `-_.` to a single `-` (PEP 503); cargo /
508
- * maven / rubygems are effectively lower-case; npm and go module paths are
509
- * matched verbatim (npm scopes and go paths are case-sensitive).
630
+ * maven / rubygems / nuget are effectively lower-case (NuGet ids are matched
631
+ * case-insensitively); npm and go module paths are matched verbatim (npm scopes
632
+ * and go paths are case-sensitive).
510
633
  */
511
634
  function normalizeName(ecosystem: DependencyEcosystem, name: string): string {
512
635
  const n = name.trim();
@@ -592,6 +715,8 @@ export function manifestEcosystem(file: string): DependencyEcosystem | null {
592
715
  if (base === "go.mod") return "go";
593
716
  if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") return "maven";
594
717
  if (base === "gemfile" || base.endsWith(".gemspec")) return "rubygems";
718
+ if (base === "packages.config" || base === "directory.packages.props" || base.endsWith(".csproj"))
719
+ return "nuget";
595
720
  return null;
596
721
  }
597
722
 
@@ -657,6 +782,20 @@ function candidateNames(ecosystem: DependencyEcosystem, content: string): string
657
782
  }
658
783
  break;
659
784
  }
785
+ case "nuget": {
786
+ // NuGet manifests are XML. `.csproj` / `Directory.Packages.props` declare
787
+ // `<PackageReference Include="Name" … />`; `packages.config` uses
788
+ // `<package id="Name" … />`. Pull the Include= / id= attribute value from
789
+ // each. The `[^>]` guard keeps every match inside a single tag, so the
790
+ // scan is linear (no nested quantifiers → no catastrophic backtracking).
791
+ for (const m of content.matchAll(/<PackageReference\b[^>]*\bInclude\s*=\s*"([^"]+)"/gi)) {
792
+ names.push(m[1]);
793
+ }
794
+ for (const m of content.matchAll(/<package\b[^>]*\bid\s*=\s*"([^"]+)"/gi)) {
795
+ names.push(m[1]);
796
+ }
797
+ break;
798
+ }
660
799
  case "npm":
661
800
  break; // npm uses the JSON path, not this extractor.
662
801
  }
@@ -237,14 +237,43 @@ export const RUBY_EXTENSIONS: readonly string[] = [".rb"];
237
237
  /** C / C++ source extensions handled by the OpenSSL detector. */
238
238
  export const C_EXTENSIONS: readonly string[] = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"];
239
239
 
240
+ /**
241
+ * Prose/documentation extensions. The language-agnostic *token* detectors (SSH
242
+ * public keys, TLS cipher suites, certificate signature algorithms) must not run
243
+ * on these: a changelog or README that mentions `ssh-rsa` or `ECDHE-RSA` in a
244
+ * sentence is prose, not crypto config. (PEM/`-----BEGIN` material stays in scope
245
+ * everywhere, so a key pasted into docs is still caught.)
246
+ */
247
+ export const DOC_EXTENSIONS: readonly string[] = [
248
+ ".md",
249
+ ".markdown",
250
+ ".mdown",
251
+ ".mkd",
252
+ ".rst",
253
+ ".adoc",
254
+ ".asciidoc",
255
+ ".textile",
256
+ ".org",
257
+ ".rdoc",
258
+ ".pod",
259
+ ];
260
+
240
261
  /**
241
262
  * File-literal surfaces where a JWT/JOSE algorithm string (`"RS256"`, `"ES256"`)
242
263
  * is the same evidence regardless of language. Used to un-gate the JWT detector
243
- * from JS-only. Kept to JS/TS + Python for now (both quote the alg token, so the
244
- * regex stays precise); YAML/JSON config carry unquoted tokens and higher FP
245
- * risk, so they wait for a config-aware pass.
264
+ * from JS-only. Covers the languages whose JWT libraries pass the alg as a quoted
265
+ * token JS/TS, Python, Go (`jwt.GetSigningMethod("RS256")`) and Ruby
266
+ * (`JWT.encode(payload, key, 'RS256')`) — so the regex stays precise (`HS*` HMAC
267
+ * tokens are excluded by `RE_JWT_ALG`). Java/C# pass the alg as an *identifier*
268
+ * (`SignatureAlgorithm.RS256`), which needs its own pattern — a later pass.
269
+ * YAML/JSON config carry unquoted tokens and higher FP risk, so they wait too.
246
270
  */
247
- export const JWT_HOST_EXTENSIONS: readonly string[] = [...JS_TS_EXTENSIONS, ...PYTHON_EXTENSIONS];
271
+ export const JWT_HOST_EXTENSIONS: readonly string[] = [
272
+ ...JS_TS_EXTENSIONS,
273
+ ...PYTHON_EXTENSIONS,
274
+ ...GO_EXTENSIONS,
275
+ ...RUBY_EXTENSIONS,
276
+ ];
248
277
 
249
278
  /**
250
279
  * Extensions the scanner can actually analyze for inline crypto usage today
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { Detector, Finding, RuleMeta } from "../types.js";
18
18
  import { C_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
19
- import { CWE_BROKEN_CRYPTO } from "../cwe.js";
19
+ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
20
20
 
21
21
  const RE_C_RSA = /\bRSA_generate_key(?:_ex)?\s*\(|\bEVP_RSA_gen\s*\(/g;
22
22
  const RE_C_EC = /\bEC_KEY_generate_key\s*\(|\bEC_KEY_new_by_curve_name\s*\(/g;
@@ -24,6 +24,30 @@ const RE_C_ECDSA = /\bECDSA_do_sign\s*\(|\bECDSA_sign\s*\(/g;
24
24
  const RE_C_ECDH = /\bECDH_compute_key\s*\(/g;
25
25
  const RE_C_DSA = /\bDSA_generate_key\s*\(|\bDSA_generate_parameters(?:_ex)?\s*\(/g;
26
26
  const RE_C_DH = /\bDH_generate_key\s*\(|\bDH_generate_parameters(?:_ex)?\s*\(/g;
27
+ // Modern OpenSSL 3.x EVP API (the legacy *_generate_key forms above are
28
+ // deprecated) + libsodium — the biggest C false-negative surface (audit F1).
29
+ const RE_C_EVP_KEYGEN = /\bEVP_PKEY_(?:Q_)?keygen\s*\(|\bEVP_PKEY_paramgen\s*\(/g;
30
+ const RE_C_EVP_DERIVE = /\bEVP_PKEY_derive\s*\(/g;
31
+ const RE_C_EVP_CRYPT = /\bEVP_PKEY_(?:encrypt|decrypt)\s*\(/g;
32
+ const RE_C_EVP_SIGN = /\bEVP_DigestSign(?:Init)?\s*\(|\bEVP_DigestVerify(?:Init)?\s*\(/g;
33
+ // X25519: crypto_box / crypto_kx key pairs (incl. the fully-qualified
34
+ // curve25519xsalsa20poly1305 form) plus the low-level crypto_scalarmult
35
+ // primitive — all classical Curve25519 key agreement (audit recall gap).
36
+ const RE_C_SODIUM_BOX =
37
+ /\bcrypto_box_(?:curve25519xsalsa20poly1305_)?(?:seed_)?keypair\s*\(|\bcrypto_kx_keypair\s*\(|\bcrypto_scalarmult_(?:curve25519|base)\s*\(/g;
38
+ // EdDSA: crypto_sign key pairs, including the explicit _ed25519 forms and the
39
+ // deterministic seed_keypair variants (audit recall gap: _ed25519_keypair).
40
+ const RE_C_SODIUM_SIGN = /\bcrypto_sign_(?:ed25519_)?(?:seed_)?keypair\s*\(/g;
41
+ // Legacy verify / decrypt counterparts to the *_sign / *_encrypt rules above
42
+ // (audit F4-c): the classic OpenSSL RSA/ECDSA verification and RSA raw
43
+ // decryption call forms that the modern EVP + legacy keygen rules don't cover.
44
+ const RE_C_ECDSA_VERIFY = /\bECDSA_verify\s*\(/g;
45
+ const RE_C_RSA_VERIFY = /\bRSA_verify\s*\(/g;
46
+ const RE_C_RSA_CRYPT = /\bRSA_public_encrypt\s*\(|\bRSA_private_decrypt\s*\(/g;
47
+ // C/OpenSSL legacy TLS configuration (mirrors source.ts tlsDetector): forcing a
48
+ // deprecated protocol version or disabling certificate verification.
49
+ const RE_C_TLS_VERSION = /\bTLSv1_method\b|\bSSLv3_method\b/g;
50
+ const RE_C_TLS_VERIFY_NONE = /\bSSL_VERIFY_NONE\b/g;
27
51
 
28
52
  const RULE_C_RSA: RuleMeta = {
29
53
  id: "c-rsa-keygen",
@@ -104,6 +128,153 @@ const RULE_C_DH: RuleMeta = {
104
128
  message:
105
129
  "Finite-field Diffie-Hellman (C/OpenSSL) is broken by Shor's algorithm (harvest-now-decrypt-later).",
106
130
  };
131
+ const RULE_C_EVP_KEYGEN: RuleMeta = {
132
+ id: "c-evp-keygen",
133
+ title: "C/OpenSSL EVP key generation",
134
+ description: "OpenSSL 3.x EVP_PKEY_keygen / EVP_PKEY_Q_keygen / paramgen",
135
+ category: "key-exchange",
136
+ severity: "high",
137
+ confidence: "high",
138
+ algorithm: "unknown",
139
+ hndl: true,
140
+ cwe: CWE_BROKEN_CRYPTO,
141
+ message:
142
+ "Generates an asymmetric key via the OpenSSL 3.x EVP API (the key type — RSA/EC/DH/X25519 — is set on the CTX). Treated conservatively as key-exchange-capable (harvest-now-decrypt-later).",
143
+ remediation:
144
+ "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
145
+ };
146
+ const RULE_C_EVP_DERIVE: RuleMeta = {
147
+ id: "c-evp-derive",
148
+ title: "C/OpenSSL EVP key agreement",
149
+ description: "OpenSSL 3.x EVP_PKEY_derive (ECDH / DH shared secret)",
150
+ category: "key-exchange",
151
+ severity: "high",
152
+ confidence: "high",
153
+ algorithm: "ECDH",
154
+ hndl: true,
155
+ cwe: CWE_BROKEN_CRYPTO,
156
+ message:
157
+ "Derives an (EC)DH shared secret via the OpenSSL EVP API — broken by Shor's algorithm (harvest-now-decrypt-later).",
158
+ };
159
+ const RULE_C_EVP_CRYPT: RuleMeta = {
160
+ id: "c-evp-pkey-crypt",
161
+ title: "C/OpenSSL EVP public-key encryption",
162
+ description: "OpenSSL 3.x EVP_PKEY_encrypt / EVP_PKEY_decrypt (RSA)",
163
+ category: "kem",
164
+ severity: "high",
165
+ confidence: "high",
166
+ algorithm: "RSA",
167
+ hndl: true,
168
+ cwe: CWE_BROKEN_CRYPTO,
169
+ message:
170
+ "RSA public-key encryption/decryption via the OpenSSL EVP API is harvest-now-decrypt-later exposed.",
171
+ };
172
+ const RULE_C_EVP_SIGN: RuleMeta = {
173
+ id: "c-evp-sign",
174
+ title: "C/OpenSSL EVP signing",
175
+ description: "OpenSSL 3.x EVP_DigestSign* / EVP_DigestVerify*",
176
+ category: "signature",
177
+ severity: "high",
178
+ confidence: "high",
179
+ algorithm: "unknown",
180
+ hndl: false,
181
+ cwe: CWE_BROKEN_CRYPTO,
182
+ message:
183
+ "Classical signature via the OpenSSL EVP API (RSA/ECDSA/EdDSA) is forgeable by a quantum attacker.",
184
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
185
+ };
186
+ const RULE_C_SODIUM_BOX: RuleMeta = {
187
+ id: "c-libsodium-box",
188
+ title: "libsodium X25519 key pair",
189
+ description:
190
+ "libsodium crypto_box / crypto_kx keypair + crypto_scalarmult (X25519 key agreement)",
191
+ category: "key-exchange",
192
+ severity: "medium",
193
+ confidence: "high",
194
+ algorithm: "X25519",
195
+ hndl: true,
196
+ cwe: CWE_BROKEN_CRYPTO,
197
+ message:
198
+ "libsodium crypto_box uses X25519 key agreement — modern but classical, and harvest-now-decrypt-later exposed.",
199
+ };
200
+ const RULE_C_SODIUM_SIGN: RuleMeta = {
201
+ id: "c-libsodium-sign",
202
+ title: "libsodium Ed25519 key pair",
203
+ description: "libsodium crypto_sign(_ed25519)(_seed)_keypair (Ed25519 signatures)",
204
+ category: "signature",
205
+ severity: "low",
206
+ confidence: "high",
207
+ algorithm: "EdDSA",
208
+ hndl: false,
209
+ cwe: CWE_BROKEN_CRYPTO,
210
+ message:
211
+ "libsodium crypto_sign uses Ed25519 signatures — classical and forgeable by a quantum attacker.",
212
+ };
213
+ const RULE_C_ECDSA_VERIFY: RuleMeta = {
214
+ id: "c-ecdsa-verify",
215
+ title: "C/OpenSSL ECDSA signature verification",
216
+ description: "OpenSSL ECDSA_verify",
217
+ category: "signature",
218
+ severity: "high",
219
+ confidence: "high",
220
+ algorithm: "ECDSA",
221
+ hndl: false,
222
+ cwe: CWE_BROKEN_CRYPTO,
223
+ message:
224
+ "Classical ECDSA verification (C/OpenSSL) trusts signatures forgeable by a quantum attacker.",
225
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
226
+ };
227
+ const RULE_C_RSA_VERIFY: RuleMeta = {
228
+ id: "c-rsa-verify",
229
+ title: "C/OpenSSL RSA signature verification",
230
+ description: "OpenSSL RSA_verify",
231
+ category: "signature",
232
+ severity: "high",
233
+ confidence: "high",
234
+ algorithm: "RSA",
235
+ hndl: false,
236
+ cwe: CWE_BROKEN_CRYPTO,
237
+ message:
238
+ "Classical RSA signature verification (C/OpenSSL) trusts signatures forgeable by a quantum attacker.",
239
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
240
+ };
241
+ const RULE_C_RSA_CRYPT: RuleMeta = {
242
+ id: "c-rsa-crypt",
243
+ title: "C/OpenSSL RSA public-key encryption",
244
+ description: "OpenSSL RSA_public_encrypt / RSA_private_decrypt",
245
+ category: "kem",
246
+ severity: "high",
247
+ confidence: "high",
248
+ algorithm: "RSA",
249
+ hndl: true,
250
+ cwe: CWE_BROKEN_CRYPTO,
251
+ message:
252
+ "Legacy RSA public-key encryption/decryption (C/OpenSSL) is harvest-now-decrypt-later exposed.",
253
+ };
254
+ const RULE_C_TLS_VERSION: RuleMeta = {
255
+ id: "c-tls-legacy-version",
256
+ title: "Legacy TLS/SSL version pinned (C/OpenSSL)",
257
+ description: "OpenSSL TLSv1_method / SSLv3_method",
258
+ category: "tls",
259
+ severity: "medium",
260
+ confidence: "high",
261
+ hndl: false,
262
+ cwe: CWE_WEAK_STRENGTH,
263
+ message: "TLS 1.0 / SSLv3 are deprecated and insecure; require TLS 1.3.",
264
+ remediation: "Use TLS_method() with a minimum of TLS 1.3 and prefer PQC-hybrid key exchange.",
265
+ };
266
+ const RULE_C_TLS_VERIFY_NONE: RuleMeta = {
267
+ id: "c-tls-verify-none",
268
+ title: "TLS certificate verification disabled (C/OpenSSL)",
269
+ description: "OpenSSL SSL_VERIFY_NONE",
270
+ category: "tls",
271
+ severity: "high",
272
+ confidence: "high",
273
+ hndl: false,
274
+ cwe: CWE_CERT_VALIDATION,
275
+ message: "SSL_VERIFY_NONE disables TLS certificate verification (MITM risk).",
276
+ remediation: "Use SSL_VERIFY_PEER and verify certificates properly.",
277
+ };
107
278
 
108
279
  /** Detects classical asymmetric crypto in C/C++ (OpenSSL). */
109
280
  export const cDetector: Detector = {
@@ -111,7 +282,25 @@ export const cDetector: Detector = {
111
282
  description: "Classical asymmetric crypto in C/C++ (OpenSSL)",
112
283
  scope: "source",
113
284
  language: "c",
114
- rules: [RULE_C_RSA, RULE_C_EC, RULE_C_ECDSA, RULE_C_ECDH, RULE_C_DSA, RULE_C_DH],
285
+ rules: [
286
+ RULE_C_RSA,
287
+ RULE_C_EC,
288
+ RULE_C_ECDSA,
289
+ RULE_C_ECDH,
290
+ RULE_C_DSA,
291
+ RULE_C_DH,
292
+ RULE_C_EVP_KEYGEN,
293
+ RULE_C_EVP_DERIVE,
294
+ RULE_C_EVP_CRYPT,
295
+ RULE_C_EVP_SIGN,
296
+ RULE_C_SODIUM_BOX,
297
+ RULE_C_SODIUM_SIGN,
298
+ RULE_C_ECDSA_VERIFY,
299
+ RULE_C_RSA_VERIFY,
300
+ RULE_C_RSA_CRYPT,
301
+ RULE_C_TLS_VERSION,
302
+ RULE_C_TLS_VERIFY_NONE,
303
+ ],
115
304
  appliesTo: (f) => hasExtension(f, C_EXTENSIONS),
116
305
  detect({ file, content }): Finding[] {
117
306
  const findings: Finding[] = [];
@@ -127,6 +316,17 @@ export const cDetector: Detector = {
127
316
  add(RE_C_ECDH, RULE_C_ECDH);
128
317
  add(RE_C_DSA, RULE_C_DSA);
129
318
  add(RE_C_DH, RULE_C_DH);
319
+ add(RE_C_EVP_KEYGEN, RULE_C_EVP_KEYGEN);
320
+ add(RE_C_EVP_DERIVE, RULE_C_EVP_DERIVE);
321
+ add(RE_C_EVP_CRYPT, RULE_C_EVP_CRYPT);
322
+ add(RE_C_EVP_SIGN, RULE_C_EVP_SIGN);
323
+ add(RE_C_SODIUM_BOX, RULE_C_SODIUM_BOX);
324
+ add(RE_C_SODIUM_SIGN, RULE_C_SODIUM_SIGN);
325
+ add(RE_C_ECDSA_VERIFY, RULE_C_ECDSA_VERIFY);
326
+ add(RE_C_RSA_VERIFY, RULE_C_RSA_VERIFY);
327
+ add(RE_C_RSA_CRYPT, RULE_C_RSA_CRYPT);
328
+ add(RE_C_TLS_VERSION, RULE_C_TLS_VERSION);
329
+ add(RE_C_TLS_VERIFY_NONE, RULE_C_TLS_VERIFY_NONE);
130
330
  return findings;
131
331
  },
132
332
  };