@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
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Crypto-agility manifest: an agent-consumable crypto-posture document.
3
+ *
4
+ * A small, versioned JSON manifest a project publishes at a well-known URL
5
+ * (`/.well-known/crypto-agility.json`, see docs/CRYPTO-AGILITY-MANIFEST.md) so any
6
+ * agent, scanner, or CI bot can read the project's cryptographic posture the way it
7
+ * reads `security.txt` or `robots.txt`. It is a compact, decision-oriented summary
8
+ * DERIVED from a qScan result: the readiness score, the count of quantum-vulnerable
9
+ * findings by severity, the CBOM's algorithm families, an optional attestation URL,
10
+ * and the migration-policy deadlines.
11
+ *
12
+ * This module owns two pure operations and their shared schema:
13
+ * - {@link buildCryptoAgilityManifest}: derive a manifest from a {@link ScanResult}.
14
+ * - {@link validateCryptoAgilityManifest}: check an untrusted value against the
15
+ * schema, returning a list of problems (used by the local validator; a network
16
+ * fetch-and-validate lives on the website, never in this offline-boundary tool).
17
+ *
18
+ * Zero dependencies; hand-rolled JSON shape and validation so the package stays
19
+ * runtime-dependency-free.
20
+ */
21
+
22
+ import { toCbom } from "./cbom.js";
23
+ import { PQC_STANDARDS } from "./standards.js";
24
+ import { SEVERITY_ORDER } from "./severity.js";
25
+ import type { CryptoPolicy } from "./policy.js";
26
+ import type { ScanResult, Severity } from "./types.js";
27
+ import { VERSION } from "./version.js";
28
+
29
+ /**
30
+ * Schema version of the manifest this module emits and validates. Bumped only on a
31
+ * breaking change to the manifest shape; consumers key their parsing off it.
32
+ */
33
+ export const CRYPTO_AGILITY_MANIFEST_VERSION = 1;
34
+
35
+ /** The conventional path a manifest is served from, relative to the site origin. */
36
+ export const CRYPTO_AGILITY_WELL_KNOWN_PATH = "/.well-known/crypto-agility.json";
37
+
38
+ /** A single algorithm family in use, mirroring the CBOM's grouping. */
39
+ export interface CryptoAgilityFamily {
40
+ /** The algorithm family (e.g. `RSA`, `ECDH`, `ECDSA`), or `unknown`. */
41
+ family: string;
42
+ /** How many findings referenced this family. */
43
+ count: number;
44
+ /** Always true today: every detected family is classical (Shor-broken). */
45
+ quantumVulnerable: boolean;
46
+ }
47
+
48
+ /** The project's cryptographic posture, distilled from the scan inventory. */
49
+ export interface CryptoAgilityPosture {
50
+ /** qScan readiness score, 0–100 (100 = no classical asymmetric crypto found). */
51
+ readinessScore: number;
52
+ /**
53
+ * Whether the project negotiates hybrid post-quantum key exchange. qScan is a
54
+ * static scanner and cannot observe a negotiated TLS group, so this is `null`
55
+ * ("not determined by this generator") unless the operator asserts it
56
+ * (`--hybrid-kex` / `--no-hybrid-kex`). A live probe (qProbe) or the website can
57
+ * fill it authoritatively.
58
+ */
59
+ hybridKexInUse: boolean | null;
60
+ /** Count of quantum-vulnerable findings, total and broken down by severity. */
61
+ quantumVulnerable: {
62
+ total: number;
63
+ bySeverity: Record<Severity, number>;
64
+ };
65
+ /** How many findings are exposed to harvest-now-decrypt-later. */
66
+ hndlExposedCount: number;
67
+ }
68
+
69
+ /** A compact summary of the full CBOM, linking to it by serial number. */
70
+ export interface CryptoAgilityCbomSummary {
71
+ /** The CycloneDX `serialNumber` of the full CBOM this summary was derived from. */
72
+ serialNumber: string;
73
+ /** Number of distinct cryptographic-asset components in the full CBOM. */
74
+ assetCount: number;
75
+ /** Algorithm families in use, most-referenced first. */
76
+ algorithmFamilies: CryptoAgilityFamily[];
77
+ }
78
+
79
+ /** The migration policy the project declares it is measured against. */
80
+ export interface CryptoAgilityPolicy {
81
+ /** Human label for the policy source (a standard name, or `operator-declared`). */
82
+ source: string;
83
+ /** Year after which classical public-key crypto is deprecated. */
84
+ deprecateClassicalAfter: number;
85
+ /** Year after which classical public-key crypto is disallowed. */
86
+ disallowClassicalAfter: number;
87
+ /** Operator-declared migration deadline (ISO date / year), or `null`. */
88
+ transitionDeadline: string | null;
89
+ /** Citation for the deadlines (spec / publication). */
90
+ citation: string;
91
+ }
92
+
93
+ /** The full crypto-agility manifest. */
94
+ export interface CryptoAgilityManifest {
95
+ /** Manifest schema version ({@link CRYPTO_AGILITY_MANIFEST_VERSION}). */
96
+ version: number;
97
+ /** Discriminator so a consumer can tell this document apart from other JSON. */
98
+ manifestType: "crypto-agility";
99
+ /** ISO 8601 timestamp the manifest was generated (caller-supplied). */
100
+ generatedAt: string;
101
+ /** What produced the manifest. */
102
+ generator: { name: string; version: string };
103
+ /** What the manifest describes. */
104
+ subject: { root: string; repository: string | null; commit: string | null };
105
+ /** The distilled cryptographic posture. */
106
+ posture: CryptoAgilityPosture;
107
+ /** A compact summary of the CBOM. */
108
+ cbomSummary: CryptoAgilityCbomSummary;
109
+ /** Optional link to a quantakrypto (or other) posture credential. */
110
+ attestation?: { url: string };
111
+ /** The declared migration policy / deadlines. */
112
+ policy: CryptoAgilityPolicy;
113
+ }
114
+
115
+ /** Inputs the CLI/runtime supplies that are not derivable from the scan. */
116
+ export interface CryptoAgilityManifestOptions {
117
+ /**
118
+ * ISO 8601 generation timestamp. The caller supplies it (`Date.now()` is fine in
119
+ * the CLI runtime) so the builder itself stays pure and deterministic.
120
+ */
121
+ generatedAt: string;
122
+ /** Optional URL to a posture credential (recorded verbatim, never fetched here). */
123
+ attestationUrl?: string;
124
+ /**
125
+ * Operator assertion of hybrid-KEX use. `undefined` leaves the manifest's
126
+ * `hybridKexInUse` as `null` (not determined).
127
+ */
128
+ hybridKexInUse?: boolean;
129
+ /** Optional org crypto policy; its `transitionDeadline` overlays the defaults. */
130
+ policy?: CryptoPolicy;
131
+ /** Repository URL (e.g. `GITHUB_REPOSITORY`); omitted → null. */
132
+ repository?: string;
133
+ /** Commit SHA (e.g. `GITHUB_SHA`); omitted → null. */
134
+ commit?: string;
135
+ }
136
+
137
+ /**
138
+ * Build a crypto-agility manifest from a scan result. Pure: every runtime input
139
+ * (timestamp, attestation, hybrid-KEX assertion, policy) arrives via `opts`.
140
+ *
141
+ * The posture and CBOM summary are derived straight from the scan's inventory and
142
+ * its CycloneDX CBOM, so the manifest can never disagree with the full `--cbom` /
143
+ * `--format json` outputs of the same scan.
144
+ */
145
+ export function buildCryptoAgilityManifest(
146
+ result: ScanResult,
147
+ opts: CryptoAgilityManifestOptions,
148
+ ): CryptoAgilityManifest {
149
+ const cbom = toCbom(result);
150
+ const inv = result.inventory;
151
+
152
+ const algorithmFamilies: CryptoAgilityFamily[] = Object.entries(inv.byAlgorithm)
153
+ .filter(([, count]) => (count ?? 0) > 0)
154
+ .map(([family, count]) => ({
155
+ family,
156
+ count: count as number,
157
+ // Every family the detectors surface is classical, hence quantum-vulnerable
158
+ // by construction (mirrors cbom.ts `isQuantumVulnerable`).
159
+ quantumVulnerable: true,
160
+ }))
161
+ .sort((a, b) => b.count - a.count || a.family.localeCompare(b.family));
162
+
163
+ const bySeverity = { ...inv.bySeverity };
164
+
165
+ // Default policy: the NIST IR 8547 transition timeline. An operator crypto policy
166
+ // (`--policy`) does not override the standards deadlines but layers its own
167
+ // `transitionDeadline` on top and re-labels the source as operator-declared.
168
+ const timeline = PQC_STANDARDS.transitionTimeline;
169
+ const policy: CryptoAgilityPolicy = {
170
+ source: opts.policy ? "operator-declared" : timeline.source,
171
+ deprecateClassicalAfter: timeline.deprecateAfter,
172
+ disallowClassicalAfter: timeline.disallowAfter,
173
+ transitionDeadline: opts.policy?.transitionDeadline ?? null,
174
+ citation: timeline.source,
175
+ };
176
+
177
+ const manifest: CryptoAgilityManifest = {
178
+ version: CRYPTO_AGILITY_MANIFEST_VERSION,
179
+ manifestType: "crypto-agility",
180
+ generatedAt: opts.generatedAt,
181
+ generator: { name: "qScan", version: result.toolVersion || VERSION },
182
+ subject: {
183
+ root: result.root,
184
+ repository: opts.repository ?? null,
185
+ commit: opts.commit ?? null,
186
+ },
187
+ posture: {
188
+ readinessScore: inv.readinessScore,
189
+ hybridKexInUse: opts.hybridKexInUse ?? null,
190
+ quantumVulnerable: {
191
+ total: result.findings.length,
192
+ bySeverity,
193
+ },
194
+ hndlExposedCount: inv.hndlCount,
195
+ },
196
+ cbomSummary: {
197
+ serialNumber: cbom.serialNumber,
198
+ assetCount: cbom.components.length,
199
+ algorithmFamilies,
200
+ },
201
+ ...(opts.attestationUrl ? { attestation: { url: opts.attestationUrl } } : {}),
202
+ policy,
203
+ };
204
+ return manifest;
205
+ }
206
+
207
+ /** Outcome of {@link validateCryptoAgilityManifest}. */
208
+ export interface ManifestValidation {
209
+ /** True when the value satisfies the manifest schema. */
210
+ valid: boolean;
211
+ /** One human-readable message per problem found (empty when `valid`). */
212
+ errors: string[];
213
+ }
214
+
215
+ /** True for a plain (non-array, non-null) object. */
216
+ function isObject(v: unknown): v is Record<string, unknown> {
217
+ return typeof v === "object" && v !== null && !Array.isArray(v);
218
+ }
219
+
220
+ /**
221
+ * Validate an untrusted value against the crypto-agility manifest schema.
222
+ *
223
+ * Checks structure, required fields, types, and the schema version. Purely local:
224
+ * it parses an already-in-memory value and NEVER performs I/O or network fetches;
225
+ * a fetch-and-validate of a remote manifest is a website-side concern, deliberately
226
+ * kept out of this offline-boundary tool. Returns every problem it finds rather than
227
+ * throwing on the first, so a caller can report them all at once.
228
+ */
229
+ export function validateCryptoAgilityManifest(value: unknown): ManifestValidation {
230
+ const errors: string[] = [];
231
+ const err = (m: string): void => void errors.push(m);
232
+
233
+ if (!isObject(value)) {
234
+ return { valid: false, errors: ["manifest must be a JSON object"] };
235
+ }
236
+
237
+ // version
238
+ if (value.version === undefined) {
239
+ err("missing required field: version");
240
+ } else if (typeof value.version !== "number" || !Number.isInteger(value.version)) {
241
+ err("version must be an integer");
242
+ } else if (value.version !== CRYPTO_AGILITY_MANIFEST_VERSION) {
243
+ err(
244
+ `unsupported version ${value.version} (this validator understands version ${CRYPTO_AGILITY_MANIFEST_VERSION})`,
245
+ );
246
+ }
247
+
248
+ // manifestType
249
+ if (value.manifestType === undefined) {
250
+ err("missing required field: manifestType");
251
+ } else if (value.manifestType !== "crypto-agility") {
252
+ err('manifestType must be the string "crypto-agility"');
253
+ }
254
+
255
+ // generatedAt
256
+ if (value.generatedAt === undefined) {
257
+ err("missing required field: generatedAt");
258
+ } else if (typeof value.generatedAt !== "string" || value.generatedAt.length === 0) {
259
+ err("generatedAt must be a non-empty ISO 8601 string");
260
+ } else if (Number.isNaN(Date.parse(value.generatedAt))) {
261
+ err("generatedAt must be a parseable ISO 8601 timestamp");
262
+ }
263
+
264
+ // generator
265
+ if (value.generator === undefined) {
266
+ err("missing required field: generator");
267
+ } else if (!isObject(value.generator)) {
268
+ err("generator must be an object");
269
+ } else {
270
+ if (typeof value.generator.name !== "string") err("generator.name must be a string");
271
+ if (typeof value.generator.version !== "string") err("generator.version must be a string");
272
+ }
273
+
274
+ // subject
275
+ if (value.subject === undefined) {
276
+ err("missing required field: subject");
277
+ } else if (!isObject(value.subject)) {
278
+ err("subject must be an object");
279
+ } else {
280
+ if (typeof value.subject.root !== "string") err("subject.root must be a string");
281
+ if (value.subject.repository !== null && typeof value.subject.repository !== "string") {
282
+ err("subject.repository must be a string or null");
283
+ }
284
+ if (value.subject.commit !== null && typeof value.subject.commit !== "string") {
285
+ err("subject.commit must be a string or null");
286
+ }
287
+ }
288
+
289
+ // posture
290
+ validatePosture(value.posture, err);
291
+
292
+ // cbomSummary
293
+ validateCbomSummary(value.cbomSummary, err);
294
+
295
+ // attestation (optional)
296
+ if (value.attestation !== undefined) {
297
+ if (!isObject(value.attestation)) {
298
+ err("attestation must be an object when present");
299
+ } else if (typeof value.attestation.url !== "string" || value.attestation.url.length === 0) {
300
+ err("attestation.url must be a non-empty string");
301
+ }
302
+ }
303
+
304
+ // policy
305
+ validatePolicy(value.policy, err);
306
+
307
+ return { valid: errors.length === 0, errors };
308
+ }
309
+
310
+ /** Validate the `posture` block. */
311
+ function validatePosture(posture: unknown, err: (m: string) => void): void {
312
+ if (posture === undefined) {
313
+ err("missing required field: posture");
314
+ return;
315
+ }
316
+ if (!isObject(posture)) {
317
+ err("posture must be an object");
318
+ return;
319
+ }
320
+ if (
321
+ typeof posture.readinessScore !== "number" ||
322
+ posture.readinessScore < 0 ||
323
+ posture.readinessScore > 100
324
+ ) {
325
+ err("posture.readinessScore must be a number between 0 and 100");
326
+ }
327
+ if (posture.hybridKexInUse !== null && typeof posture.hybridKexInUse !== "boolean") {
328
+ err("posture.hybridKexInUse must be a boolean or null");
329
+ }
330
+ if (typeof posture.hndlExposedCount !== "number" || posture.hndlExposedCount < 0) {
331
+ err("posture.hndlExposedCount must be a non-negative number");
332
+ }
333
+ const qv = posture.quantumVulnerable;
334
+ if (!isObject(qv)) {
335
+ err("posture.quantumVulnerable must be an object");
336
+ return;
337
+ }
338
+ if (typeof qv.total !== "number" || qv.total < 0) {
339
+ err("posture.quantumVulnerable.total must be a non-negative number");
340
+ }
341
+ if (!isObject(qv.bySeverity)) {
342
+ err("posture.quantumVulnerable.bySeverity must be an object");
343
+ } else {
344
+ for (const sev of SEVERITY_ORDER) {
345
+ if (typeof qv.bySeverity[sev] !== "number") {
346
+ err(`posture.quantumVulnerable.bySeverity.${sev} must be a number`);
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ /** Validate the `cbomSummary` block. */
353
+ function validateCbomSummary(summary: unknown, err: (m: string) => void): void {
354
+ if (summary === undefined) {
355
+ err("missing required field: cbomSummary");
356
+ return;
357
+ }
358
+ if (!isObject(summary)) {
359
+ err("cbomSummary must be an object");
360
+ return;
361
+ }
362
+ if (typeof summary.serialNumber !== "string") err("cbomSummary.serialNumber must be a string");
363
+ if (typeof summary.assetCount !== "number" || summary.assetCount < 0) {
364
+ err("cbomSummary.assetCount must be a non-negative number");
365
+ }
366
+ if (!Array.isArray(summary.algorithmFamilies)) {
367
+ err("cbomSummary.algorithmFamilies must be an array");
368
+ return;
369
+ }
370
+ summary.algorithmFamilies.forEach((fam, i) => {
371
+ if (!isObject(fam)) {
372
+ err(`cbomSummary.algorithmFamilies[${i}] must be an object`);
373
+ return;
374
+ }
375
+ if (typeof fam.family !== "string")
376
+ err(`cbomSummary.algorithmFamilies[${i}].family must be a string`);
377
+ if (typeof fam.count !== "number" || fam.count < 0) {
378
+ err(`cbomSummary.algorithmFamilies[${i}].count must be a non-negative number`);
379
+ }
380
+ if (typeof fam.quantumVulnerable !== "boolean") {
381
+ err(`cbomSummary.algorithmFamilies[${i}].quantumVulnerable must be a boolean`);
382
+ }
383
+ });
384
+ }
385
+
386
+ /** Validate the `policy` block. */
387
+ function validatePolicy(policy: unknown, err: (m: string) => void): void {
388
+ if (policy === undefined) {
389
+ err("missing required field: policy");
390
+ return;
391
+ }
392
+ if (!isObject(policy)) {
393
+ err("policy must be an object");
394
+ return;
395
+ }
396
+ if (typeof policy.source !== "string") err("policy.source must be a string");
397
+ if (typeof policy.deprecateClassicalAfter !== "number") {
398
+ err("policy.deprecateClassicalAfter must be a number (year)");
399
+ }
400
+ if (typeof policy.disallowClassicalAfter !== "number") {
401
+ err("policy.disallowClassicalAfter must be a number (year)");
402
+ }
403
+ if (policy.transitionDeadline !== null && typeof policy.transitionDeadline !== "string") {
404
+ err("policy.transitionDeadline must be a string or null");
405
+ }
406
+ if (typeof policy.citation !== "string") err("policy.citation must be a string");
407
+ }
@@ -401,6 +401,14 @@ export const vulnerableDependencies: VulnerableDependency[] = [
401
401
  algorithms: ["X25519", "EdDSA"],
402
402
  severity: "low",
403
403
  },
404
+ {
405
+ name: "tink",
406
+ ecosystem: "pypi",
407
+ reason:
408
+ "Google Tink (Python) — classical RSA/ECDSA/Ed25519 signatures and ECIES hybrid encryption.",
409
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
410
+ severity: "high",
411
+ },
404
412
 
405
413
  // --- crates.io (Rust) ---
406
414
  {
@@ -513,6 +521,21 @@ export const vulnerableDependencies: VulnerableDependency[] = [
513
521
  algorithms: ["ECDSA", "ECDH"],
514
522
  severity: "medium",
515
523
  },
524
+ {
525
+ name: "github.com/tink-crypto/tink-go/v2",
526
+ ecosystem: "go",
527
+ reason:
528
+ "Google Tink (Go) — classical RSA/ECDSA/Ed25519 signatures and ECIES hybrid encryption.",
529
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
530
+ severity: "high",
531
+ },
532
+ {
533
+ name: "github.com/google/tink/go",
534
+ ecosystem: "go",
535
+ reason: "Google Tink (legacy Go module) — classical RSA/ECDSA/Ed25519 and ECIES hybrid.",
536
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
537
+ severity: "high",
538
+ },
516
539
 
517
540
  // --- Maven (Java) ---
518
541
  {
@@ -559,6 +582,14 @@ export const vulnerableDependencies: VulnerableDependency[] = [
559
582
  severity: "medium",
560
583
  hndl: false,
561
584
  },
585
+ {
586
+ name: "tink",
587
+ ecosystem: "maven",
588
+ reason:
589
+ "Google Tink (com.google.crypto.tink) — classical RSA/ECDSA/Ed25519 signatures and ECIES hybrid encryption.",
590
+ algorithms: ["RSA", "ECDSA", "EdDSA", "ECIES"],
591
+ severity: "high",
592
+ },
562
593
 
563
594
  // --- RubyGems ---
564
595
  {
@@ -623,14 +654,83 @@ export const vulnerableDependencies: VulnerableDependency[] = [
623
654
  severity: "medium",
624
655
  hndl: false,
625
656
  },
657
+
658
+ // --- Composer (PHP) ---
659
+ {
660
+ name: "phpseclib/phpseclib",
661
+ ecosystem: "composer",
662
+ reason: "Pure-PHP RSA / DSA / ECDSA / Diffie-Hellman public-key crypto and X.509.",
663
+ algorithms: ["RSA", "DSA", "ECDSA", "DH"],
664
+ severity: "high",
665
+ },
666
+ {
667
+ name: "paragonie/sodium_compat",
668
+ ecosystem: "composer",
669
+ reason: "Pure-PHP libsodium polyfill: X25519 key agreement and Ed25519 signatures.",
670
+ algorithms: ["X25519", "EdDSA"],
671
+ severity: "low",
672
+ },
673
+ {
674
+ name: "paragonie/halite",
675
+ ecosystem: "composer",
676
+ reason: "libsodium wrapper: X25519 key agreement and Ed25519 signatures.",
677
+ algorithms: ["X25519", "EdDSA"],
678
+ severity: "low",
679
+ },
680
+ {
681
+ name: "paragonie/paseto",
682
+ ecosystem: "composer",
683
+ reason: "PASETO public tokens signed with classical Ed25519 (v2/v4) or RSA.",
684
+ algorithms: ["EdDSA", "RSA"],
685
+ severity: "medium",
686
+ hndl: false,
687
+ },
688
+ {
689
+ name: "firebase/php-jwt",
690
+ ecosystem: "composer",
691
+ reason: "PHP JWT signing/verification with classical RS*/ES* algorithms.",
692
+ algorithms: ["RSA", "ECDSA"],
693
+ severity: "medium",
694
+ hndl: false,
695
+ },
696
+ {
697
+ name: "lcobucci/jwt",
698
+ ecosystem: "composer",
699
+ reason: "PHP JWT with classical RSA/ECDSA signature algorithms.",
700
+ algorithms: ["RSA", "ECDSA"],
701
+ severity: "medium",
702
+ hndl: false,
703
+ },
704
+ {
705
+ name: "web-token/jwt-framework",
706
+ ecosystem: "composer",
707
+ reason: "JOSE (JWS/JWE) framework: classical RSA/ECDSA signatures and ECDH-ES key agreement.",
708
+ algorithms: ["RSA", "ECDSA", "ECDH"],
709
+ severity: "medium",
710
+ },
711
+ {
712
+ name: "mdanter/ecc",
713
+ ecosystem: "composer",
714
+ reason: "Pure-PHP elliptic-curve ECDSA signatures and ECDH key agreement.",
715
+ algorithms: ["ECDSA", "ECDH"],
716
+ severity: "high",
717
+ },
718
+ {
719
+ name: "simplito/elliptic-php",
720
+ ecosystem: "composer",
721
+ reason: "secp256k1 / NIST-curve ECDSA and ECDH in pure PHP (blockchain keys).",
722
+ algorithms: ["ECDSA", "ECDH"],
723
+ severity: "high",
724
+ },
626
725
  ];
627
726
 
628
727
  /**
629
728
  * Normalise a package name for matching within its ecosystem. PyPI is
630
729
  * case-insensitive and folds runs of `-_.` to a single `-` (PEP 503); cargo /
631
- * maven / rubygems / nuget are effectively lower-case (NuGet ids are matched
632
- * case-insensitively); npm and go module paths are matched verbatim (npm scopes
633
- * and go paths are case-sensitive).
730
+ * maven / rubygems / nuget / composer are effectively lower-case (NuGet ids are
731
+ * matched case-insensitively; Composer `vendor/package` names are lower-cased by
732
+ * Packagist); npm and go module paths are matched verbatim (npm scopes and go
733
+ * paths are case-sensitive).
634
734
  */
635
735
  function normalizeName(ecosystem: DependencyEcosystem, name: string): string {
636
736
  const n = name.trim();
@@ -718,6 +818,7 @@ export function manifestEcosystem(file: string): DependencyEcosystem | null {
718
818
  if (base === "gemfile" || base.endsWith(".gemspec")) return "rubygems";
719
819
  if (base === "packages.config" || base === "directory.packages.props" || base.endsWith(".csproj"))
720
820
  return "nuget";
821
+ if (base === "composer.json" || base === "composer.lock") return "composer";
721
822
  return null;
722
823
  }
723
824
 
@@ -797,6 +898,20 @@ function candidateNames(ecosystem: DependencyEcosystem, content: string): string
797
898
  }
798
899
  break;
799
900
  }
901
+ case "composer": {
902
+ // composer.json `require`/`require-dev` maps are keyed by "vendor/package";
903
+ // composer.lock lists `{"name": "vendor/package", …}`. Both are JSON, so
904
+ // grab every quoted `vendor/package` token (a lower-case vendor + package
905
+ // separated by a single `/`). PHP platform requirements ("php", "ext-*")
906
+ // carry no slash and so are naturally excluded; the curated DB filters the
907
+ // rest, so over-capture is safe.
908
+ for (const m of content.matchAll(
909
+ /["']([a-z0-9](?:[a-z0-9._-]*[a-z0-9])?\/[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)["']/gi,
910
+ )) {
911
+ names.push(m[1]);
912
+ }
913
+ break;
914
+ }
800
915
  case "npm":
801
916
  break; // npm uses the JSON path, not this extractor.
802
917
  }
@@ -893,10 +1008,33 @@ export function scanManifest(file: string, content: string): Finding[] {
893
1008
  return sortByTitle(findings);
894
1009
  }
895
1010
 
1011
+ /**
1012
+ * Resolve an npm `npm:` alias spec to the real package it installs, or null when
1013
+ * the spec is not an alias. An aliased dependency records the alias as the map
1014
+ * KEY and `npm:<real-name>@<range>` as the VALUE (in package.json and in a
1015
+ * lockfile entry's `version`), so the vulnerable package hides behind a benign
1016
+ * alias name. Keeps a leading scope `@` (only a version `@` past index 0 is a
1017
+ * separator): `npm:node-forge@^1` → `node-forge`, `npm:@scope/pkg@1` → `@scope/pkg`.
1018
+ */
1019
+ function npmAliasTarget(spec: string): string | null {
1020
+ if (!spec.startsWith("npm:")) return null;
1021
+ const rest = spec.slice(4);
1022
+ const at = rest.lastIndexOf("@");
1023
+ const name = at > 0 ? rest.slice(0, at) : rest;
1024
+ return name || null;
1025
+ }
1026
+
1027
+ /** Guard against a pathological / hostile lockfile nesting the walk unbounded. */
1028
+ const NPM_TREE_MAX_DEPTH = 64;
1029
+
896
1030
  /**
897
1031
  * npm manifest scan.
898
- * - `package.json`: dependencies / devDependencies / peerDependencies / optionalDependencies.
899
- * - `package-lock.json` (v2/v3): the `packages` map keys (node_modules/<name>).
1032
+ * - `package.json`: dependencies / devDependencies / peerDependencies / optionalDependencies,
1033
+ * including `npm:`-aliased entries (the alias key hides the real package name).
1034
+ * - `package-lock.json` v1: the NESTED `dependencies` tree, where each entry may carry
1035
+ * its own `dependencies` (transitive deps) and an `npm:` alias in `version`.
1036
+ * - `package-lock.json` v2/v3: the flat `packages` map keys (node_modules/<name>),
1037
+ * plus each entry's own `name` field (set for aliased installs).
900
1038
  */
901
1039
  function scanNpmManifest(
902
1040
  content: string,
@@ -914,26 +1052,54 @@ function scanNpmManifest(
914
1052
  const found = new Set<string>();
915
1053
  const obj = json as Record<string, unknown>;
916
1054
 
917
- const collectFromRecord = (rec: unknown): void => {
918
- if (rec === null || typeof rec !== "object") return;
919
- for (const key of Object.keys(rec as Record<string, unknown>)) {
920
- if (db.has(key)) found.add(key);
1055
+ const add = (name: string): void => {
1056
+ if (db.has(name)) found.add(name);
1057
+ };
1058
+
1059
+ /**
1060
+ * Walk a dependency map that may be either a flat package.json map
1061
+ * (name → version-range string) or a legacy package-lock v1 tree (name →
1062
+ * `{ version, requires, dependencies }`, arbitrarily nested). Matches keys,
1063
+ * resolves `npm:` aliases from string values and entry `version` fields, and
1064
+ * recurses into a nested `dependencies` sub-tree.
1065
+ */
1066
+ const walkDepTree = (deps: unknown, depth: number): void => {
1067
+ if (depth > NPM_TREE_MAX_DEPTH || deps === null || typeof deps !== "object") return;
1068
+ for (const [name, node] of Object.entries(deps as Record<string, unknown>)) {
1069
+ add(name);
1070
+ if (typeof node === "string") {
1071
+ // package.json form: the value is a range, possibly `npm:<real>@<range>`.
1072
+ const aliased = npmAliasTarget(node);
1073
+ if (aliased) add(aliased);
1074
+ } else if (node !== null && typeof node === "object") {
1075
+ const rec = node as Record<string, unknown>;
1076
+ if (typeof rec.version === "string") {
1077
+ const aliased = npmAliasTarget(rec.version);
1078
+ if (aliased) add(aliased);
1079
+ }
1080
+ walkDepTree(rec.dependencies, depth + 1); // v1 nests transitive deps here
1081
+ }
921
1082
  }
922
1083
  };
923
1084
 
924
- collectFromRecord(obj.dependencies);
925
- collectFromRecord(obj.devDependencies);
926
- collectFromRecord(obj.peerDependencies);
927
- collectFromRecord(obj.optionalDependencies);
1085
+ walkDepTree(obj.dependencies, 0);
1086
+ walkDepTree(obj.devDependencies, 0);
1087
+ walkDepTree(obj.peerDependencies, 0);
1088
+ walkDepTree(obj.optionalDependencies, 0);
928
1089
 
929
1090
  const packages = obj.packages;
930
1091
  if (packages !== null && typeof packages === "object") {
931
- for (const key of Object.keys(packages as Record<string, unknown>)) {
1092
+ for (const [key, entry] of Object.entries(packages as Record<string, unknown>)) {
932
1093
  if (!key) continue; // root package entry
933
1094
  const marker = "node_modules/";
934
1095
  const idx = key.lastIndexOf(marker);
935
- const name = idx >= 0 ? key.slice(idx + marker.length) : key;
936
- if (db.has(name)) found.add(name);
1096
+ add(idx >= 0 ? key.slice(idx + marker.length) : key);
1097
+ // Aliased install: the entry's own `name` field is the real package, which
1098
+ // differs from the alias embedded in the node_modules/<alias> key.
1099
+ if (entry !== null && typeof entry === "object") {
1100
+ const realName = (entry as Record<string, unknown>).name;
1101
+ if (typeof realName === "string") add(realName);
1102
+ }
937
1103
  }
938
1104
  }
939
1105