@quantakrypto/core 0.4.4 → 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 (302) hide show
  1. package/README.md +76 -27
  2. package/dist/cbom-merge.d.ts.map +1 -1
  3. package/dist/cbom-merge.js +30 -8
  4. package/dist/cbom-merge.js.map +1 -1
  5. package/dist/cbom.d.ts +5 -0
  6. package/dist/cbom.d.ts.map +1 -1
  7. package/dist/cbom.js +132 -37
  8. package/dist/cbom.js.map +1 -1
  9. package/dist/comments.d.ts +2 -23
  10. package/dist/comments.d.ts.map +1 -1
  11. package/dist/comments.js +24 -5
  12. package/dist/comments.js.map +1 -1
  13. package/dist/crypto-agility.d.ts +158 -0
  14. package/dist/crypto-agility.d.ts.map +1 -0
  15. package/dist/crypto-agility.js +285 -0
  16. package/dist/crypto-agility.js.map +1 -0
  17. package/dist/dependencies.d.ts.map +1 -1
  18. package/dist/dependencies.js +180 -18
  19. package/dist/dependencies.js.map +1 -1
  20. package/dist/detect-utils.d.ts +55 -21
  21. package/dist/detect-utils.d.ts.map +1 -1
  22. package/dist/detect-utils.js +133 -7
  23. package/dist/detect-utils.js.map +1 -1
  24. package/dist/detectors/age.d.ts +16 -0
  25. package/dist/detectors/age.d.ts.map +1 -0
  26. package/dist/detectors/age.js +40 -0
  27. package/dist/detectors/age.js.map +1 -0
  28. package/dist/detectors/ansible.d.ts +13 -0
  29. package/dist/detectors/ansible.d.ts.map +1 -0
  30. package/dist/detectors/ansible.js +108 -0
  31. package/dist/detectors/ansible.js.map +1 -0
  32. package/dist/detectors/bicep.d.ts +20 -0
  33. package/dist/detectors/bicep.d.ts.map +1 -0
  34. package/dist/detectors/bicep.js +74 -0
  35. package/dist/detectors/bicep.js.map +1 -0
  36. package/dist/detectors/c.d.ts.map +1 -1
  37. package/dist/detectors/c.js +30 -3
  38. package/dist/detectors/c.js.map +1 -1
  39. package/dist/detectors/cicd.d.ts.map +1 -1
  40. package/dist/detectors/cicd.js +18 -5
  41. package/dist/detectors/cicd.js.map +1 -1
  42. package/dist/detectors/cloud-kms.d.ts +8 -5
  43. package/dist/detectors/cloud-kms.d.ts.map +1 -1
  44. package/dist/detectors/cloud-kms.js +62 -20
  45. package/dist/detectors/cloud-kms.js.map +1 -1
  46. package/dist/detectors/cloudformation.d.ts +8 -0
  47. package/dist/detectors/cloudformation.d.ts.map +1 -1
  48. package/dist/detectors/cloudformation.js +41 -10
  49. package/dist/detectors/cloudformation.js.map +1 -1
  50. package/dist/detectors/codesign.d.ts +50 -0
  51. package/dist/detectors/codesign.d.ts.map +1 -0
  52. package/dist/detectors/codesign.js +143 -0
  53. package/dist/detectors/codesign.js.map +1 -0
  54. package/dist/detectors/csharp.d.ts.map +1 -1
  55. package/dist/detectors/csharp.js +13 -3
  56. package/dist/detectors/csharp.js.map +1 -1
  57. package/dist/detectors/dart.d.ts +35 -0
  58. package/dist/detectors/dart.d.ts.map +1 -0
  59. package/dist/detectors/dart.js +177 -0
  60. package/dist/detectors/dart.js.map +1 -0
  61. package/dist/detectors/database.d.ts +5 -3
  62. package/dist/detectors/database.d.ts.map +1 -1
  63. package/dist/detectors/database.js +45 -8
  64. package/dist/detectors/database.js.map +1 -1
  65. package/dist/detectors/dkim.d.ts +54 -0
  66. package/dist/detectors/dkim.d.ts.map +1 -0
  67. package/dist/detectors/dkim.js +92 -0
  68. package/dist/detectors/dkim.js.map +1 -0
  69. package/dist/detectors/dnssec.d.ts.map +1 -1
  70. package/dist/detectors/dnssec.js +19 -10
  71. package/dist/detectors/dnssec.js.map +1 -1
  72. package/dist/detectors/elixir.d.ts.map +1 -1
  73. package/dist/detectors/elixir.js +34 -2
  74. package/dist/detectors/elixir.js.map +1 -1
  75. package/dist/detectors/java.d.ts.map +1 -1
  76. package/dist/detectors/java.js +33 -3
  77. package/dist/detectors/java.js.map +1 -1
  78. package/dist/detectors/jose.d.ts.map +1 -1
  79. package/dist/detectors/jose.js +13 -3
  80. package/dist/detectors/jose.js.map +1 -1
  81. package/dist/detectors/jwk.d.ts +7 -7
  82. package/dist/detectors/jwk.d.ts.map +1 -1
  83. package/dist/detectors/jwk.js +44 -8
  84. package/dist/detectors/jwk.js.map +1 -1
  85. package/dist/detectors/k8s.d.ts.map +1 -1
  86. package/dist/detectors/k8s.js +5 -2
  87. package/dist/detectors/k8s.js.map +1 -1
  88. package/dist/detectors/keystore.d.ts +19 -0
  89. package/dist/detectors/keystore.d.ts.map +1 -0
  90. package/dist/detectors/keystore.js +76 -0
  91. package/dist/detectors/keystore.js.map +1 -0
  92. package/dist/detectors/mesh.d.ts +7 -8
  93. package/dist/detectors/mesh.d.ts.map +1 -1
  94. package/dist/detectors/mesh.js +17 -35
  95. package/dist/detectors/mesh.js.map +1 -1
  96. package/dist/detectors/messaging.d.ts +2 -2
  97. package/dist/detectors/messaging.d.ts.map +1 -1
  98. package/dist/detectors/messaging.js +19 -12
  99. package/dist/detectors/messaging.js.map +1 -1
  100. package/dist/detectors/objc.d.ts +41 -0
  101. package/dist/detectors/objc.d.ts.map +1 -0
  102. package/dist/detectors/objc.js +146 -0
  103. package/dist/detectors/objc.js.map +1 -0
  104. package/dist/detectors/openpgp.d.ts +20 -0
  105. package/dist/detectors/openpgp.d.ts.map +1 -0
  106. package/dist/detectors/openpgp.js +151 -0
  107. package/dist/detectors/openpgp.js.map +1 -0
  108. package/dist/detectors/pem.d.ts.map +1 -1
  109. package/dist/detectors/pem.js +32 -0
  110. package/dist/detectors/pem.js.map +1 -1
  111. package/dist/detectors/php.d.ts +2 -1
  112. package/dist/detectors/php.d.ts.map +1 -1
  113. package/dist/detectors/php.js +7 -3
  114. package/dist/detectors/php.js.map +1 -1
  115. package/dist/detectors/pkcs11.d.ts +17 -0
  116. package/dist/detectors/pkcs11.d.ts.map +1 -0
  117. package/dist/detectors/pkcs11.js +87 -0
  118. package/dist/detectors/pkcs11.js.map +1 -0
  119. package/dist/detectors/proxy.d.ts +52 -0
  120. package/dist/detectors/proxy.d.ts.map +1 -0
  121. package/dist/detectors/proxy.js +157 -0
  122. package/dist/detectors/proxy.js.map +1 -0
  123. package/dist/detectors/pulumi.d.ts +19 -0
  124. package/dist/detectors/pulumi.d.ts.map +1 -0
  125. package/dist/detectors/pulumi.js +80 -0
  126. package/dist/detectors/pulumi.js.map +1 -0
  127. package/dist/detectors/rust.d.ts.map +1 -1
  128. package/dist/detectors/rust.js +5 -0
  129. package/dist/detectors/rust.js.map +1 -1
  130. package/dist/detectors/secrets.d.ts +4 -1
  131. package/dist/detectors/secrets.d.ts.map +1 -1
  132. package/dist/detectors/secrets.js +5 -21
  133. package/dist/detectors/secrets.js.map +1 -1
  134. package/dist/detectors/solidity.d.ts +50 -0
  135. package/dist/detectors/solidity.d.ts.map +1 -0
  136. package/dist/detectors/solidity.js +108 -0
  137. package/dist/detectors/solidity.js.map +1 -0
  138. package/dist/detectors/source.d.ts.map +1 -1
  139. package/dist/detectors/source.js +105 -6
  140. package/dist/detectors/source.js.map +1 -1
  141. package/dist/detectors/spire.d.ts +49 -0
  142. package/dist/detectors/spire.d.ts.map +1 -0
  143. package/dist/detectors/spire.js +77 -0
  144. package/dist/detectors/spire.js.map +1 -0
  145. package/dist/detectors/ssh-ca.d.ts +72 -0
  146. package/dist/detectors/ssh-ca.d.ts.map +1 -0
  147. package/dist/detectors/ssh-ca.js +119 -0
  148. package/dist/detectors/ssh-ca.js.map +1 -0
  149. package/dist/detectors/supply-chain.d.ts +17 -0
  150. package/dist/detectors/supply-chain.d.ts.map +1 -0
  151. package/dist/detectors/supply-chain.js +89 -0
  152. package/dist/detectors/supply-chain.js.map +1 -0
  153. package/dist/detectors/swift.d.ts +22 -0
  154. package/dist/detectors/swift.d.ts.map +1 -0
  155. package/dist/detectors/swift.js +120 -0
  156. package/dist/detectors/swift.js.map +1 -0
  157. package/dist/detectors/terraform.d.ts.map +1 -1
  158. package/dist/detectors/terraform.js +35 -8
  159. package/dist/detectors/terraform.js.map +1 -1
  160. package/dist/detectors/vault.d.ts +15 -0
  161. package/dist/detectors/vault.d.ts.map +1 -0
  162. package/dist/detectors/vault.js +97 -0
  163. package/dist/detectors/vault.js.map +1 -0
  164. package/dist/detectors/vpn.d.ts +8 -8
  165. package/dist/detectors/vpn.d.ts.map +1 -1
  166. package/dist/detectors/vpn.js +12 -38
  167. package/dist/detectors/vpn.js.map +1 -1
  168. package/dist/detectors/weak-hash.d.ts +71 -0
  169. package/dist/detectors/weak-hash.d.ts.map +1 -0
  170. package/dist/detectors/weak-hash.js +116 -0
  171. package/dist/detectors/weak-hash.js.map +1 -0
  172. package/dist/detectors/webauthn.d.ts +61 -0
  173. package/dist/detectors/webauthn.d.ts.map +1 -0
  174. package/dist/detectors/webauthn.js +109 -0
  175. package/dist/detectors/webauthn.js.map +1 -0
  176. package/dist/detectors/xmldsig.d.ts +23 -0
  177. package/dist/detectors/xmldsig.d.ts.map +1 -0
  178. package/dist/detectors/xmldsig.js +84 -0
  179. package/dist/detectors/xmldsig.js.map +1 -0
  180. package/dist/evidence.d.ts +74 -4
  181. package/dist/evidence.d.ts.map +1 -1
  182. package/dist/evidence.js +71 -0
  183. package/dist/evidence.js.map +1 -1
  184. package/dist/hndl.d.ts +241 -0
  185. package/dist/hndl.d.ts.map +1 -0
  186. package/dist/hndl.js +752 -0
  187. package/dist/hndl.js.map +1 -0
  188. package/dist/index.d.ts +18 -11
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +21 -11
  191. package/dist/index.js.map +1 -1
  192. package/dist/inventory.d.ts +0 -2
  193. package/dist/inventory.d.ts.map +1 -1
  194. package/dist/inventory.js +15 -6
  195. package/dist/inventory.js.map +1 -1
  196. package/dist/parallel.d.ts.map +1 -1
  197. package/dist/parallel.js +22 -23
  198. package/dist/parallel.js.map +1 -1
  199. package/dist/registry.d.ts +9 -4
  200. package/dist/registry.d.ts.map +1 -1
  201. package/dist/registry.js +51 -4
  202. package/dist/registry.js.map +1 -1
  203. package/dist/remediate-pipeline.d.ts +0 -3
  204. package/dist/remediate-pipeline.d.ts.map +1 -1
  205. package/dist/remediate-pipeline.js +1 -1
  206. package/dist/remediate-pipeline.js.map +1 -1
  207. package/dist/remediation.d.ts +10 -0
  208. package/dist/remediation.d.ts.map +1 -1
  209. package/dist/remediation.js +32 -0
  210. package/dist/remediation.js.map +1 -1
  211. package/dist/report.d.ts +17 -0
  212. package/dist/report.d.ts.map +1 -1
  213. package/dist/report.js +106 -20
  214. package/dist/report.js.map +1 -1
  215. package/dist/scan-worker.js +5 -3
  216. package/dist/scan-worker.js.map +1 -1
  217. package/dist/scan.d.ts.map +1 -1
  218. package/dist/scan.js +35 -6
  219. package/dist/scan.js.map +1 -1
  220. package/dist/standards-profiles.d.ts +58 -0
  221. package/dist/standards-profiles.d.ts.map +1 -0
  222. package/dist/standards-profiles.js +100 -0
  223. package/dist/standards-profiles.js.map +1 -0
  224. package/dist/types.d.ts +7 -6
  225. package/dist/types.d.ts.map +1 -1
  226. package/dist/types.js.map +1 -1
  227. package/dist/version.d.ts +1 -1
  228. package/dist/version.js +1 -1
  229. package/dist/version.js.map +1 -1
  230. package/dist/vex.d.ts +39 -0
  231. package/dist/vex.d.ts.map +1 -0
  232. package/dist/vex.js +87 -0
  233. package/dist/vex.js.map +1 -0
  234. package/dist/walk.d.ts +39 -4
  235. package/dist/walk.d.ts.map +1 -1
  236. package/dist/walk.js +62 -10
  237. package/dist/walk.js.map +1 -1
  238. package/package.json +1 -1
  239. package/src/cbom-merge.ts +38 -8
  240. package/src/cbom.ts +153 -39
  241. package/src/comments.ts +25 -6
  242. package/src/crypto-agility.ts +407 -0
  243. package/src/dependencies.ts +182 -16
  244. package/src/detect-utils.ts +133 -11
  245. package/src/detectors/age.ts +60 -0
  246. package/src/detectors/ansible.ts +140 -0
  247. package/src/detectors/bicep.ts +107 -0
  248. package/src/detectors/c.ts +31 -3
  249. package/src/detectors/cicd.ts +18 -5
  250. package/src/detectors/cloud-kms.ts +79 -25
  251. package/src/detectors/cloudformation.ts +45 -10
  252. package/src/detectors/codesign.ts +214 -0
  253. package/src/detectors/csharp.ts +13 -3
  254. package/src/detectors/dart.ts +237 -0
  255. package/src/detectors/database.ts +64 -11
  256. package/src/detectors/dkim.ts +172 -0
  257. package/src/detectors/dnssec.ts +19 -10
  258. package/src/detectors/elixir.ts +33 -2
  259. package/src/detectors/java.ts +37 -3
  260. package/src/detectors/jose.ts +19 -3
  261. package/src/detectors/jwk.ts +59 -17
  262. package/src/detectors/k8s.ts +5 -2
  263. package/src/detectors/keystore.ts +109 -0
  264. package/src/detectors/mesh.ts +21 -45
  265. package/src/detectors/messaging.ts +22 -14
  266. package/src/detectors/objc.ts +209 -0
  267. package/src/detectors/openpgp.ts +194 -0
  268. package/src/detectors/pem.ts +30 -0
  269. package/src/detectors/php.ts +9 -4
  270. package/src/detectors/pkcs11.ts +113 -0
  271. package/src/detectors/proxy.ts +236 -0
  272. package/src/detectors/pulumi.ts +106 -0
  273. package/src/detectors/rust.ts +5 -0
  274. package/src/detectors/secrets.ts +15 -26
  275. package/src/detectors/solidity.ts +176 -0
  276. package/src/detectors/source.ts +110 -5
  277. package/src/detectors/spire.ts +143 -0
  278. package/src/detectors/ssh-ca.ts +215 -0
  279. package/src/detectors/supply-chain.ts +117 -0
  280. package/src/detectors/swift.ts +149 -0
  281. package/src/detectors/terraform.ts +42 -8
  282. package/src/detectors/vault.ts +120 -0
  283. package/src/detectors/vpn.ts +20 -48
  284. package/src/detectors/weak-hash.ts +217 -0
  285. package/src/detectors/webauthn.ts +205 -0
  286. package/src/detectors/xmldsig.ts +115 -0
  287. package/src/evidence.ts +127 -4
  288. package/src/hndl.ts +1012 -0
  289. package/src/index.ts +93 -13
  290. package/src/inventory.ts +15 -7
  291. package/src/parallel.ts +21 -20
  292. package/src/registry.ts +51 -4
  293. package/src/remediate-pipeline.ts +1 -1
  294. package/src/remediation.ts +39 -0
  295. package/src/report.ts +120 -20
  296. package/src/scan-worker.ts +5 -3
  297. package/src/scan.ts +41 -6
  298. package/src/standards-profiles.ts +136 -0
  299. package/src/types.ts +18 -5
  300. package/src/version.ts +1 -1
  301. package/src/vex.ts +122 -0
  302. package/src/walk.ts +80 -12
package/src/scan.ts CHANGED
@@ -13,7 +13,15 @@ import { readFile, stat } from "node:fs/promises";
13
13
  import * as path from "node:path";
14
14
 
15
15
  import type { Detector, Finding, ScanOptions, ScanResult } from "./types.js";
16
- import { walkFiles, toPosix, isBinaryPath, looksMinified, matchesAny } from "./walk.js";
16
+ import {
17
+ walkFiles,
18
+ toPosix,
19
+ isBinaryPath,
20
+ isKeystorePath,
21
+ looksMinified,
22
+ matchesAny,
23
+ DEFAULT_MAX_FILE_SIZE,
24
+ } from "./walk.js";
17
25
  import { isAnalyzableSource } from "./detect-utils.js";
18
26
  import {
19
27
  stripCommentFindings,
@@ -26,7 +34,16 @@ import {
26
34
  * A match inside a string (e.g. `t.Error("SigningMethodPS256 …")`) is prose and
27
35
  * is dropped. Rules that legitimately match quoted tokens are NOT listed here.
28
36
  */
29
- const CODE_ONLY_RULES: ReadonlySet<string> = new Set(["go-jwt-signingmethod"]);
37
+ const CODE_ONLY_RULES: ReadonlySet<string> = new Set([
38
+ "go-jwt-signingmethod",
39
+ // Identifier-form JWT alg constants (jjwt `SignatureAlgorithm.RS256`, auth0
40
+ // `Algorithm.RSA256`, C# `SecurityAlgorithms.*`, Rust `Algorithm::RS256`). Like the
41
+ // Go rule, these are only meaningful as code; the SAME token inside a string literal
42
+ // (an error message that names/forbids the alg) is prose, not a usage.
43
+ "java-jwt-alg",
44
+ "csharp-jwt-alg",
45
+ "rust-jwt-algorithm",
46
+ ]);
30
47
  import { hashContent, loadCache, rulesetFingerprint, saveCache } from "./cache.js";
31
48
  import type { CacheEntry } from "./cache.js";
32
49
  import { builtinDetectors, defaultRegistry, detectorScope } from "./registry.js";
@@ -169,23 +186,41 @@ export async function scan(options: ScanOptions): Promise<ScanResult> {
169
186
 
170
187
  options.onFile?.(reportedPath);
171
188
 
189
+ // Cryptographic keystores (.jks/.p12/…) are binary; read them byte-preserving
190
+ // (latin1) so the keystore detector can inspect magic bytes.
191
+ const keystore = isKeystorePath(reportedPath);
192
+ // Keystores are read whole into memory as latin1. The walker already caps file
193
+ // size, but the explicit-file-list (incremental) path does not — so stat and
194
+ // skip an oversized binary here to avoid reading a huge encrypted archive
195
+ // (`.gpg`) into a string.
196
+ if (keystore) {
197
+ try {
198
+ const { size } = await stat(absPath);
199
+ if (size > (options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE)) continue;
200
+ } catch {
201
+ unreadable += 1;
202
+ continue;
203
+ }
204
+ }
172
205
  let content: string;
173
206
  try {
174
- content = await readFile(absPath, "utf8");
207
+ content = await readFile(absPath, keystore ? "latin1" : "utf8");
175
208
  } catch {
176
209
  unreadable += 1; // permissions / vanished / decode failure — tracked, not silent.
177
210
  continue;
178
211
  }
179
212
 
180
213
  // Skip machine-minified / generated content (unless explicitly enabled).
181
- // Manifests are always scanned (their findings are dependency findings).
182
- if (!scanMinified && !isManifestFile(reportedPath) && looksMinified(content)) {
214
+ // Manifests and keystores are always scanned.
215
+ if (!scanMinified && !isManifestFile(reportedPath) && !keystore && looksMinified(content)) {
183
216
  skippedMinified += 1;
184
217
  continue;
185
218
  }
186
219
 
187
220
  // Cumulative-byte budget: enforced once this file's bytes are accounted for.
188
- bytesScanned += Buffer.byteLength(content, "utf8");
221
+ // Use the read encoding so a latin1 keystore is counted at its on-disk size
222
+ // (a utf8 byteLength would double-count every byte ≥ 0x80).
223
+ bytesScanned += Buffer.byteLength(content, keystore ? "latin1" : "utf8");
189
224
  if (typeof maxBytes === "number" && bytesScanned > maxBytes) {
190
225
  throw new BudgetExceededError(`maxBytes budget exceeded (limit: ${maxBytes}).`);
191
226
  }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Selectable STANDARDS PROFILES — the regime a scan's remediation, deadlines, and
3
+ * hybrid guidance are tailored to. Different national/regional authorities agree on
4
+ * the PQC primitives (ML-KEM / ML-DSA) but diverge on two things this tool must not
5
+ * hardcode: (a) the required PARAMETER SETS (a commercial ML-KEM-768 vs a
6
+ * national-security ML-KEM-1024), and (b) the HYBRID STANCE — whether classical+PQC
7
+ * hybridization is required, recommended, or optional during the transition. Baking
8
+ * in NIST/CNSA's "hybrids optional" is wrong for an ANSSI or BSI audience, where
9
+ * hybrid is required. `--profile <id>` selects the regime; `--policy` composes an
10
+ * org's own exceptions on top.
11
+ *
12
+ * Like {@link PqcStandards}, every profile carries a citation + `asOf` date and is
13
+ * re-verified on the quarterly standards cadence. These are guidance summaries, not
14
+ * legal advice — consult the cited source of record.
15
+ */
16
+
17
+ /** Whether classical+PQC hybridization is required during the transition, per regime. */
18
+ export type HybridStance = "required" | "recommended" | "optional";
19
+
20
+ /** A regime's PQC guidance profile. */
21
+ export interface StandardsProfile {
22
+ /** Stable id used by `--profile` (kebab-case). */
23
+ readonly id: string;
24
+ /** Human-readable name. */
25
+ readonly name: string;
26
+ /** The authority / document of record. */
27
+ readonly authority: string;
28
+ /** The KEM / signature parameter sets this regime calls for. */
29
+ readonly paramSets: { readonly kem: string; readonly signature: string };
30
+ /** Whether hybridization is required / recommended / optional under this regime. */
31
+ readonly hybridStance: HybridStance;
32
+ /** One-line regime-specific hybrid guidance surfaced in remediation. */
33
+ readonly hybridGuidance: string;
34
+ /** Year after which classical public-key crypto is deprecated under this regime. */
35
+ readonly deprecateAfter: number;
36
+ /** Year after which it is disallowed. */
37
+ readonly disallowAfter: number;
38
+ /** Spec identifier / publication of record. */
39
+ readonly citation: string;
40
+ /** `YYYY-MM` — when this profile was last verified against its source. */
41
+ readonly asOf: string;
42
+ }
43
+
44
+ /** The default profile id when `--profile` is not given. */
45
+ export const DEFAULT_PROFILE_ID = "nist";
46
+
47
+ /**
48
+ * Built-in regime profiles. Facts reflect each authority's published PQC-transition
49
+ * position as of the `asOf` date; verify against the cited source before relying on a
50
+ * deadline or a hybrid mandate for a compliance decision.
51
+ */
52
+ export const STANDARDS_PROFILES: Readonly<Record<string, StandardsProfile>> = {
53
+ nist: {
54
+ id: "nist",
55
+ name: "NIST (general / commercial)",
56
+ authority: "NIST",
57
+ paramSets: { kem: "ML-KEM-768 (FIPS 203)", signature: "ML-DSA-65 (FIPS 204)" },
58
+ hybridStance: "recommended",
59
+ hybridGuidance:
60
+ "Hybrid key establishment (e.g. X25519MLKEM768) is permitted and recommended during the transition; pure ML-KEM is also acceptable (SP 800-227 / IR 8547).",
61
+ deprecateAfter: 2030,
62
+ disallowAfter: 2035,
63
+ citation: "NIST IR 8547 + FIPS 203/204/205",
64
+ asOf: "2026-07",
65
+ },
66
+ "cnsa-2.0": {
67
+ id: "cnsa-2.0",
68
+ name: "NSA CNSA 2.0 (national-security systems)",
69
+ authority: "NSA",
70
+ paramSets: { kem: "ML-KEM-1024 (FIPS 203)", signature: "ML-DSA-87 (FIPS 204)" },
71
+ hybridStance: "optional",
72
+ hybridGuidance:
73
+ "CNSA 2.0 targets pure PQC and does not require hybrids; if a hybrid TLS group is used, it must be SecP384r1MLKEM1024 — X25519MLKEM768's ML-KEM-768 component is sub-CNSA.",
74
+ deprecateAfter: 2030,
75
+ disallowAfter: 2035,
76
+ citation: "NSA CNSA 2.0 (2030/2033/2035 migration milestones)",
77
+ asOf: "2026-07",
78
+ },
79
+ "bsi-tr-02102": {
80
+ id: "bsi-tr-02102",
81
+ name: "BSI TR-02102 (Germany)",
82
+ authority: "BSI",
83
+ paramSets: { kem: "ML-KEM-768 (FIPS 203)", signature: "ML-DSA-65 (FIPS 204)" },
84
+ hybridStance: "required",
85
+ hybridGuidance:
86
+ "BSI requires PQC be deployed in HYBRID with an established classical scheme during the transition (defense-in-depth); FrodoKEM is the conservative KEM alternative to ML-KEM, and XMSS/LMS are approved for firmware signing.",
87
+ deprecateAfter: 2030,
88
+ disallowAfter: 2035,
89
+ citation: "BSI TR-02102-1 (Kryptographische Verfahren)",
90
+ asOf: "2026-07",
91
+ },
92
+ anssi: {
93
+ id: "anssi",
94
+ name: "ANSSI (France)",
95
+ authority: "ANSSI",
96
+ paramSets: { kem: "ML-KEM-1024 (FIPS 203)", signature: "ML-DSA-87 (FIPS 204)" },
97
+ hybridStance: "required",
98
+ hybridGuidance:
99
+ "ANSSI requires HYBRIDIZATION (classical + PQC) throughout the transition phase and does not endorse pure PQC alone yet; use the highest parameter set for long-lived assurance.",
100
+ deprecateAfter: 2030,
101
+ disallowAfter: 2035,
102
+ citation: "ANSSI — PQC transition position papers",
103
+ asOf: "2026-07",
104
+ },
105
+ "uk-ncsc": {
106
+ id: "uk-ncsc",
107
+ name: "UK NCSC",
108
+ authority: "NCSC",
109
+ paramSets: { kem: "ML-KEM-768 (FIPS 203)", signature: "ML-DSA-65 (FIPS 204)" },
110
+ hybridStance: "recommended",
111
+ hybridGuidance:
112
+ "NCSC recommends ML-KEM / ML-DSA and is broadly agnostic on hybridization (recommended, not mandated); its migration milestones are earlier — discovery/plan by 2028, high-priority migration by 2031, complete by 2035.",
113
+ deprecateAfter: 2031,
114
+ disallowAfter: 2035,
115
+ citation: "NCSC — Preparing for quantum-safe cryptography / PQC migration timeline",
116
+ asOf: "2026-07",
117
+ },
118
+ };
119
+
120
+ /** All built-in profile ids, in a stable order (default first). */
121
+ export function standardsProfileIds(): string[] {
122
+ return [
123
+ DEFAULT_PROFILE_ID,
124
+ ...Object.keys(STANDARDS_PROFILES).filter((id) => id !== DEFAULT_PROFILE_ID),
125
+ ];
126
+ }
127
+
128
+ /** Look up a built-in profile by id, or `undefined` when unknown. */
129
+ export function getStandardsProfile(id: string): StandardsProfile | undefined {
130
+ return STANDARDS_PROFILES[id];
131
+ }
132
+
133
+ /** The default profile (NIST). Always defined. */
134
+ export function defaultStandardsProfile(): StandardsProfile {
135
+ return STANDARDS_PROFILES[DEFAULT_PROFILE_ID];
136
+ }
package/src/types.ts CHANGED
@@ -95,7 +95,15 @@ export interface Finding {
95
95
  }
96
96
 
97
97
  /** Package ecosystems the dependency scanner understands. */
98
- export type DependencyEcosystem = "npm" | "pypi" | "cargo" | "go" | "maven" | "rubygems" | "nuget";
98
+ export type DependencyEcosystem =
99
+ | "npm"
100
+ | "pypi"
101
+ | "cargo"
102
+ | "go"
103
+ | "maven"
104
+ | "rubygems"
105
+ | "nuget"
106
+ | "composer";
99
107
 
100
108
  /** A known quantum-vulnerable dependency entry. */
101
109
  export interface VulnerableDependency {
@@ -137,6 +145,10 @@ export type DetectorLanguage =
137
145
  | "php"
138
146
  | "elixir"
139
147
  | "c"
148
+ | "swift"
149
+ | "objc"
150
+ | "dart"
151
+ | "solidity"
140
152
  | "any";
141
153
 
142
154
  /**
@@ -344,10 +356,11 @@ export interface ScanResult {
344
356
  */
345
357
  diagnostics?: ScanDiagnostics;
346
358
  /**
347
- * Of `filesScanned`, how many were in a source language the scanner can
348
- * actually analyze for inline crypto (JS/TS, Python, Go, Java). When this is 0 but
349
- * `filesScanned` > 0, the readiness score reflects NO analyzable code the
350
- * crypto likely lives in an unsupported language (Go, Java, Rust, …)and
359
+ * Of `filesScanned`, how many were in a source language the scanner can actually
360
+ * analyze for inline crypto (the 13 packs: JS/TS, Python, Go, Java/Kotlin/Scala,
361
+ * C#, Rust, Ruby, PHP, Elixir, C/C++, Swift, Objective-C, Dart). When this is 0
362
+ * but `filesScanned` > 0, the readiness score reflects NO analyzable codethe
363
+ * crypto likely lives in an unsupported language (Lua, Perl, …) — and
351
364
  * reporters surface that so a bare 100/100 can't read as "safe". Optional for
352
365
  * backward compatibility with hand-built results.
353
366
  */
package/src/version.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * the scan orchestrator can import it without creating a cycle through index.ts.
4
4
  * Keep in sync with packages/core/package.json.
5
5
  */
6
- export const VERSION = "0.4.4";
6
+ export const VERSION = "0.6.0";
package/src/vex.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * OpenVEX export — turn a {@link ScanResult} into an OpenVEX 0.2.0 document so
3
+ * the quantum-readiness posture flows into the SAME supply-chain pipeline that
4
+ * already ingests CVE-based VEX (Vulnerability Exploitability eXchange).
5
+ *
6
+ * Reference: OpenVEX spec (https://openvex.dev/, `@context` v0.2.0).
7
+ *
8
+ * Modeling choice (documented, honest): PQC findings have no CVE, so each qScan
9
+ * rule becomes a synthetic vulnerability `QK-<ruleId>`. Every finding is reported
10
+ * `status: "affected"` — the classical, Shor-breakable primitive IS present in
11
+ * the product by construction — with the rule's remediation as the VEX
12
+ * `action_statement`. A `--triage` exposure verdict, when present, is surfaced in
13
+ * `status_notes` (score / priority / rationale). qScan never downgrades a finding
14
+ * to `not_affected`: only the operator can attest a mitigation, so that judgment
15
+ * is deliberately left to them (they can post-process this document).
16
+ */
17
+ import { createHash } from "node:crypto";
18
+
19
+ import type { Finding, ScanResult } from "./types.js";
20
+
21
+ /** A single OpenVEX statement: one synthetic vulnerability over its affected products. */
22
+ export interface OpenVexStatement {
23
+ vulnerability: { name: string; description?: string };
24
+ products: { "@id": string }[];
25
+ status: "not_affected" | "affected" | "fixed" | "under_investigation";
26
+ /** Recommended action for an `affected` product (the rule's remediation). */
27
+ action_statement?: string;
28
+ /** Free-form status detail — carries the `--triage` exposure verdict when present. */
29
+ status_notes?: string;
30
+ }
31
+
32
+ /** An OpenVEX 0.2.0 document. */
33
+ export interface OpenVexDocument {
34
+ "@context": "https://openvex.dev/ns/v0.2.0";
35
+ "@id": string;
36
+ author: string;
37
+ timestamp: string;
38
+ version: number;
39
+ statements: OpenVexStatement[];
40
+ }
41
+
42
+ /** Options for {@link toOpenVex}. */
43
+ export interface OpenVexOptions {
44
+ /** Document author (VEX issuer). Default: `"qScan"`. */
45
+ author?: string;
46
+ }
47
+
48
+ /**
49
+ * A stable product identifier for a finding as an IRI (OpenVEX requires product
50
+ * `@id` to be an IRI): a `file:` URI with the line in the fragment, e.g.
51
+ * `file:src/a.ts#L3`.
52
+ */
53
+ function productId(f: Finding): string {
54
+ return `file:${f.location.file}#L${f.location.line}`;
55
+ }
56
+
57
+ /**
58
+ * Build an OpenVEX 0.2.0 document from a scan result. One statement per distinct
59
+ * rule id, listing every affected `file:line` product. Output is deterministic
60
+ * (statements sorted by vulnerability name, products sorted and de-duplicated;
61
+ * the `@id` and `timestamp` derive from the result), so re-exporting the same
62
+ * scan yields byte-identical VEX.
63
+ */
64
+ export function toOpenVex(result: ScanResult, opts: OpenVexOptions = {}): OpenVexDocument {
65
+ // Group findings by rule id → one VEX statement per rule.
66
+ const byRule = new Map<string, Finding[]>();
67
+ for (const f of result.findings) {
68
+ const list = byRule.get(f.ruleId);
69
+ if (list) list.push(f);
70
+ else byRule.set(f.ruleId, [f]);
71
+ }
72
+
73
+ const statements: OpenVexStatement[] = [...byRule.entries()]
74
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
75
+ .map(([ruleId, findings]) => {
76
+ const first = findings[0] as Finding;
77
+ // De-duplicate + sort the affected products (a rule can fire many times).
78
+ const products = [...new Set(findings.map(productId))].sort().map((id) => ({ "@id": id }));
79
+
80
+ // Surface a triage verdict (if any finding for this rule carries one). Findings
81
+ // of the same rule share a rule; the most-exposed verdict is the informative one.
82
+ const triaged = findings
83
+ .filter((f) => f.triage)
84
+ .sort((a, b) => (b.triage?.exposureScore ?? 0) - (a.triage?.exposureScore ?? 0))[0]?.triage;
85
+
86
+ const statement: OpenVexStatement = {
87
+ vulnerability: {
88
+ name: `QK-${ruleId}`,
89
+ description: first.message,
90
+ },
91
+ products,
92
+ status: "affected",
93
+ action_statement: first.remediation ?? "Migrate to a NIST PQC standard (FIPS 203/204/205).",
94
+ };
95
+ if (triaged) {
96
+ statement.status_notes =
97
+ `qScan triage — exposure ${triaged.exposureScore}/100, priority ${triaged.priority}: ` +
98
+ triaged.rationale;
99
+ }
100
+ return statement;
101
+ });
102
+
103
+ // Deterministic doc id + timestamp so the same scan yields identical VEX. The
104
+ // triage `status_notes` are part of the identity: a triaged and an untriaged
105
+ // export of the same scan are DIFFERENT documents and must not share an @id.
106
+ const digest = createHash("sha256").update(`${result.root}|${result.toolVersion}`, "utf8");
107
+ for (const s of statements) {
108
+ digest.update(
109
+ `\n${s.vulnerability.name}|${s.products.map((p) => p["@id"]).join(",")}|${s.status_notes ?? ""}`,
110
+ );
111
+ }
112
+ const id = `https://quantakrypto.com/vex/${digest.digest("hex").slice(0, 16)}`;
113
+
114
+ return {
115
+ "@context": "https://openvex.dev/ns/v0.2.0",
116
+ "@id": id,
117
+ author: opts.author ?? "qScan",
118
+ timestamp: result.finishedAt,
119
+ version: 1,
120
+ statements,
121
+ };
122
+ }
package/src/walk.ts CHANGED
@@ -11,7 +11,7 @@ import * as path from "node:path";
11
11
  import { isManifestFile } from "./dependencies.js";
12
12
 
13
13
  /** Directories ignored by default (can be disabled with noDefaultIgnores). */
14
- export const DEFAULT_IGNORES: readonly string[] = [
14
+ const DEFAULT_IGNORES: readonly string[] = [
15
15
  "node_modules",
16
16
  ".git",
17
17
  "dist",
@@ -99,7 +99,7 @@ const BINARY_EXTENSIONS = new Set<string>([
99
99
  ]);
100
100
 
101
101
  /** Options accepted by {@link walkFiles}. */
102
- export interface WalkOptions {
102
+ interface WalkOptions {
103
103
  /**
104
104
  * Restrict to paths matching one of these include patterns (substring or
105
105
  * relative-path-prefix match). When omitted/empty, all files pass.
@@ -222,6 +222,37 @@ export function isBinaryPath(rel: string): boolean {
222
222
  return BINARY_EXTENSIONS.has(ext);
223
223
  }
224
224
 
225
+ /**
226
+ * Binary cryptographic key containers we DO want to scan — Java/PKCS#12 keystores
227
+ * and binary OpenPGP keyrings/messages. Unlike the images/archives above these are
228
+ * read byte-preserving (latin1) and exempted from the minified skip, so the
229
+ * keystore + openpgp detectors can identify them by magic number / packet tag.
230
+ * Kept separate from {@link BINARY_EXTENSIONS} so those detectors can opt in.
231
+ */
232
+ const KEYSTORE_EXTENSIONS = new Set<string>([
233
+ ".jks",
234
+ ".keystore",
235
+ ".jceks",
236
+ ".bks",
237
+ ".p12",
238
+ ".pfx",
239
+ // Binary OpenPGP keyrings / messages (armored .asc is text; handled elsewhere).
240
+ ".gpg",
241
+ ".pgp",
242
+ ".kbx",
243
+ ]);
244
+
245
+ /**
246
+ * True if the path is a binary cryptographic key container we scan byte-preserving.
247
+ * Uses a suffix match (not `extname`) so it agrees with the detectors' `hasExtension`
248
+ * even for a bare dotfile literally named e.g. `.gpg` (where `extname` returns "").
249
+ */
250
+ export function isKeystorePath(rel: string): boolean {
251
+ const lower = rel.toLowerCase();
252
+ for (const ext of KEYSTORE_EXTENSIONS) if (lower.endsWith(ext)) return true;
253
+ return false;
254
+ }
255
+
225
256
  /**
226
257
  * Compound / pattern extensions that mark generated or bundled output we skip
227
258
  * by default (beyond `.min.js` / `.map`, which {@link isBinaryPath} handles).
@@ -260,12 +291,34 @@ export function looksMinified(content: string): boolean {
260
291
  return avgLine > 1_000;
261
292
  }
262
293
 
294
+ /** A scannable file plus the byte size the walker already stat'd for it. */
295
+ export interface WalkedFile {
296
+ /** Relative POSIX path from the walk root. */
297
+ rel: string;
298
+ /** File size in bytes (from the walker's own stat; never re-stat downstream). */
299
+ size: number;
300
+ }
301
+
263
302
  /**
264
303
  * Recursively yield scannable file paths (relative to `root`, POSIX) under a
265
304
  * directory. If `root` points at a single file, yields just that file's
266
- * basename (subject to the size / binary filters).
305
+ * basename (subject to the size / binary filters). Thin wrapper over
306
+ * {@link walkFilesSized} that drops the size (the historical string-yielding API).
267
307
  */
268
308
  export async function* walkFiles(root: string, options: WalkOptions = {}): AsyncGenerator<string> {
309
+ for await (const f of walkFilesSized(root, options)) yield f.rel;
310
+ }
311
+
312
+ /**
313
+ * Like {@link walkFiles} but yields each file WITH the byte size the walker
314
+ * already obtained while applying the size limit. Callers that need sizes (the
315
+ * parallel scanner's byte-balanced chunking) consume this directly so they do
316
+ * not stat every file a second time (P2 double-stat elimination).
317
+ */
318
+ export async function* walkFilesSized(
319
+ root: string,
320
+ options: WalkOptions = {},
321
+ ): AsyncGenerator<WalkedFile> {
269
322
  const include = options.include ?? [];
270
323
  const exclude = options.exclude ?? [];
271
324
  const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
@@ -281,7 +334,7 @@ export async function* walkFiles(root: string, options: WalkOptions = {}): Async
281
334
  isIncluded(name, include) &&
282
335
  passesSizeLimit(name, rootStat.size, maxFileSize)
283
336
  ) {
284
- yield name;
337
+ yield { rel: name, size: rootStat.size };
285
338
  }
286
339
  return;
287
340
  }
@@ -296,20 +349,33 @@ interface WalkContext {
296
349
  ignores: readonly string[];
297
350
  }
298
351
 
352
+ /**
353
+ * Manifests bypass the normal file-size cap (they carry the whole dependency
354
+ * tree and must not be dropped), but still get a generous hard ceiling so a
355
+ * pathological or hostile lockfile can't be read unbounded and OOM the scan.
356
+ * 16 MiB comfortably covers real monorepo lockfiles (npm's own is well under 5).
357
+ */
358
+ export const MANIFEST_MAX_BYTES = 16 * 1024 * 1024;
359
+
299
360
  /**
300
361
  * True if a file passes the size limit. Dependency manifests (package.json /
301
- * package-lock.json) are exempt from the cap so large lockfiles still get
302
- * scanned for vulnerable dependencies instead of being silently dropped.
362
+ * package-lock.json / yarn.lock / …) get the larger {@link MANIFEST_MAX_BYTES}
363
+ * ceiling instead of the ordinary cap, so large lockfiles still get scanned for
364
+ * vulnerable dependencies without letting an enormous one exhaust memory.
365
+ * Exported for unit testing.
303
366
  */
304
- function passesSizeLimit(rel: string, size: number, maxFileSize: number): boolean {
305
- // Dependency manifests are always read (they can exceed the size cap but carry
306
- // the whole dependency tree). Uses the single {@link isManifestFile} definition.
307
- if (isManifestFile(rel)) return true;
367
+ export function passesSizeLimit(rel: string, size: number, maxFileSize: number): boolean {
368
+ // Uses the single {@link isManifestFile} definition.
369
+ if (isManifestFile(rel)) return size <= MANIFEST_MAX_BYTES;
308
370
  return size <= maxFileSize;
309
371
  }
310
372
 
311
373
  /** Internal recursive directory walker. `relDir` is POSIX-relative to the root. */
312
- async function* walkDir(absDir: string, relDir: string, ctx: WalkContext): AsyncGenerator<string> {
374
+ async function* walkDir(
375
+ absDir: string,
376
+ relDir: string,
377
+ ctx: WalkContext,
378
+ ): AsyncGenerator<WalkedFile> {
313
379
  let entries: Dirent[];
314
380
  try {
315
381
  entries = await readdir(absDir, { withFileTypes: true });
@@ -346,13 +412,15 @@ async function* walkDir(absDir: string, relDir: string, ctx: WalkContext): Async
346
412
  if (!manifest && isBinaryPath(rel)) continue;
347
413
  if (!manifest && isGeneratedPath(rel)) continue;
348
414
 
415
+ let size: number;
349
416
  try {
350
417
  const s = await stat(abs);
351
418
  if (!passesSizeLimit(rel, s.size, ctx.maxFileSize)) continue;
419
+ size = s.size;
352
420
  } catch {
353
421
  continue;
354
422
  }
355
423
 
356
- yield rel;
424
+ yield { rel, size };
357
425
  }
358
426
  }