@cdxgen/cdxgen 13.6.0 → 13.7.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 (264) hide show
  1. package/README.md +15 -8
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +256 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +25 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +18 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/pqc-migration.json +239 -0
  15. package/data/remediations.json +52 -0
  16. package/data/rules/cbom-security.yaml +4 -3
  17. package/data/rules/package-integrity.yaml +28 -0
  18. package/lib/audit/index.js +25 -3
  19. package/lib/cli/bomAssembly.js +80 -1
  20. package/lib/cli/cliOptions.js +121 -17
  21. package/lib/cli/index.js +609 -898
  22. package/lib/cli/jsBom.js +3 -0
  23. package/lib/cli/jvmBom.js +365 -40
  24. package/lib/cli/managedBom.js +19 -3
  25. package/lib/cli/nativeBom.js +10 -0
  26. package/lib/core/env.js +26 -0
  27. package/lib/core/fs.js +126 -3
  28. package/lib/core/globPatterns.js +218 -0
  29. package/lib/core/processTree.js +99 -12
  30. package/lib/core/projectConfig.js +332 -25
  31. package/lib/core/toolRegistry.js +15 -1
  32. package/lib/ecosystems/denoutils.js +2 -2
  33. package/lib/ecosystems/golem.js +3 -8
  34. package/lib/ecosystems/gradleutils.js +1082 -6
  35. package/lib/ecosystems/jsEvidence.js +90 -38
  36. package/lib/ecosystems/kosi.js +3 -5
  37. package/lib/ecosystems/npmVersionResolution.js +376 -0
  38. package/lib/ecosystems/parsers-python.js +89 -28
  39. package/lib/ecosystems/parsers-rust.js +117 -15
  40. package/lib/ecosystems/remote/huggingface.js +22 -0
  41. package/lib/ecosystems/rusi.js +2 -4
  42. package/lib/ecosystems/scalasem.js +2 -1
  43. package/lib/evinser/evinser.js +196 -34
  44. package/lib/evinser/scalasem.js +5 -1
  45. package/lib/helpers/agenticToolutils.js +112 -1
  46. package/lib/helpers/bomSigner.js +43 -9
  47. package/lib/helpers/jssSigner.js +1276 -0
  48. package/lib/inventory/agentBlueprint.js +193 -0
  49. package/lib/inventory/agentToolNames.js +145 -0
  50. package/lib/inventory/aiCollector.js +153 -7
  51. package/lib/inventory/aiInventory.js +44 -8
  52. package/lib/inventory/aiProviders.js +324 -0
  53. package/lib/inventory/bomUtils.js +8 -1
  54. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  55. package/lib/inventory/citations.js +31 -2
  56. package/lib/inventory/communityAiConfigParser.js +978 -80
  57. package/lib/inventory/cyclonedx20.js +152 -0
  58. package/lib/inventory/cyclonedx20Common.js +286 -0
  59. package/lib/inventory/cyclonedx20Derived.js +159 -0
  60. package/lib/inventory/cyclonedx20Parties.js +552 -0
  61. package/lib/inventory/cyclonedx20Read.js +768 -0
  62. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  63. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  64. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  65. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  66. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  67. package/lib/inventory/cyclonedx20Render.js +766 -0
  68. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  69. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  70. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  71. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  72. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  73. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  74. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  75. package/lib/inventory/cyclonedx20Tables.js +594 -0
  76. package/lib/inventory/deps.js +106 -13
  77. package/lib/inventory/dosai.js +236 -1
  78. package/lib/inventory/dosaiParsers.js +3 -6
  79. package/lib/inventory/dotnetRuntime.js +164 -14
  80. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  81. package/lib/inventory/envcontext.js +132 -38
  82. package/lib/inventory/evidenceUtils.js +2 -1
  83. package/lib/inventory/formulationParsers.js +95 -13
  84. package/lib/inventory/hbom.js +45 -6
  85. package/lib/inventory/hbomLoader.js +166 -0
  86. package/lib/inventory/mcpConfigParser.js +5 -0
  87. package/lib/inventory/occurrenceKey.js +90 -0
  88. package/lib/inventory/pqcMigration.js +426 -0
  89. package/lib/inventory/rustFormulationParser.js +21 -10
  90. package/lib/inventory/source.js +65 -6
  91. package/lib/parsers/huggingfaceManifest.js +9 -1
  92. package/lib/parsers/huggingfaceUtils.js +18 -0
  93. package/lib/parsers/kiroPermissions.js +166 -0
  94. package/lib/server/server.js +36 -34
  95. package/lib/stages/postgen/postgen.js +41 -1
  96. package/lib/stages/postgen/specVersionCompat.js +76 -0
  97. package/lib/validator/bomValidator.js +8 -1
  98. package/lib/validator/complianceEngine.js +30 -3
  99. package/lib/validator/complianceRules.js +1416 -4
  100. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  101. package/lib/validator/index.js +48 -7
  102. package/lib/validator/reporters/json.js +6 -0
  103. package/package.json +9 -9
  104. package/types/bin/repl.d.ts.map +1 -1
  105. package/types/lib/audit/index.d.ts +6 -2
  106. package/types/lib/audit/index.d.ts.map +1 -1
  107. package/types/lib/cli/bomAssembly.d.ts +16 -0
  108. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  109. package/types/lib/cli/cliOptions.d.ts +6 -0
  110. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  111. package/types/lib/cli/index.d.ts.map +1 -1
  112. package/types/lib/cli/jsBom.d.ts +2 -1
  113. package/types/lib/cli/jsBom.d.ts.map +1 -1
  114. package/types/lib/cli/jvmBom.d.ts +15 -0
  115. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  116. package/types/lib/cli/managedBom.d.ts.map +1 -1
  117. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  118. package/types/lib/core/boundedFile.d.ts +30 -0
  119. package/types/lib/core/boundedFile.d.ts.map +1 -1
  120. package/types/lib/core/env.d.ts +10 -0
  121. package/types/lib/core/env.d.ts.map +1 -1
  122. package/types/lib/core/fs.d.ts +4 -0
  123. package/types/lib/core/fs.d.ts.map +1 -1
  124. package/types/lib/core/globPatterns.d.ts +70 -0
  125. package/types/lib/core/globPatterns.d.ts.map +1 -0
  126. package/types/lib/core/paths.d.ts +9 -0
  127. package/types/lib/core/paths.d.ts.map +1 -1
  128. package/types/lib/core/processTree.d.ts +16 -5
  129. package/types/lib/core/processTree.d.ts.map +1 -1
  130. package/types/lib/core/projectConfig.d.ts +47 -8
  131. package/types/lib/core/projectConfig.d.ts.map +1 -1
  132. package/types/lib/core/toolRegistry.d.ts.map +1 -1
  133. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  134. package/types/lib/ecosystems/gradleutils.d.ts +248 -8
  135. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  136. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  137. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  139. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  140. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  141. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  142. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  143. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  144. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  145. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  146. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  147. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  148. package/types/lib/evinser/evinser.d.ts +61 -9
  149. package/types/lib/evinser/evinser.d.ts.map +1 -1
  150. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  151. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  152. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  153. package/types/lib/helpers/bomSigner.d.ts +20 -3
  154. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  155. package/types/lib/helpers/jssSigner.d.ts +288 -0
  156. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  157. package/types/lib/helpers/vsixutils.d.ts +303 -19
  158. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  159. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  160. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  161. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  162. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  163. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  164. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  165. package/types/lib/inventory/aiProviders.d.ts +85 -0
  166. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  167. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  168. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  169. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  170. package/types/lib/inventory/citations.d.ts +5 -0
  171. package/types/lib/inventory/citations.d.ts.map +1 -1
  172. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  173. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  174. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  175. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  176. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  177. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  178. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  179. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  180. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  181. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  182. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  183. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  184. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  185. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  186. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  187. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  188. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  189. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  190. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  191. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  192. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  193. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  194. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  195. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  196. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  197. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  198. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  199. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  200. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  201. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  202. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  203. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  204. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  205. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  206. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  207. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  208. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  209. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  210. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  211. package/types/lib/inventory/deps.d.ts +35 -2
  212. package/types/lib/inventory/deps.d.ts.map +1 -1
  213. package/types/lib/inventory/dosai.d.ts +19 -0
  214. package/types/lib/inventory/dosai.d.ts.map +1 -1
  215. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  216. package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
  217. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
  218. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  219. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  220. package/types/lib/inventory/envcontext.d.ts +22 -6
  221. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  222. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  224. package/types/lib/inventory/hbom.d.ts +11 -0
  225. package/types/lib/inventory/hbom.d.ts.map +1 -1
  226. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  227. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  228. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  229. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  230. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  231. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  232. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  233. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  234. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  235. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  236. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  237. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  238. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  239. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  240. package/types/lib/inventory/source.d.ts +15 -0
  241. package/types/lib/inventory/source.d.ts.map +1 -1
  242. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  243. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  244. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  245. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  246. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  247. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  248. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  249. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  250. package/types/lib/server/server.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  252. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  253. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  254. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  255. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  256. package/types/lib/validator/bomValidator.d.ts +5 -1
  257. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  258. package/types/lib/validator/complianceEngine.d.ts +4 -4
  259. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  260. package/types/lib/validator/complianceRules.d.ts +14 -2
  261. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  262. package/types/lib/validator/index.d.ts +10 -0
  263. package/types/lib/validator/index.d.ts.map +1 -1
  264. package/types/lib/validator/reporters/json.d.ts.map +1 -1
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Post-quantum migration risk for cryptographic assets (cdxgen/cdxgen#4592).
3
+ *
4
+ * The facts come from data/pqc-migration.json: the classes of NIST IR 8547
5
+ * (final, October 2026) Tables 2, 4 and 8 and section 4.1.3, plus FIPS 186-5
6
+ * for DSA, each with its members (algorithm families, OIDs and normalised
7
+ * names), the transition the source states and the rationale text. This
8
+ * module classifies a component and builds the risk entry; it adds no fact of
9
+ * its own.
10
+ *
11
+ * Classification of an algorithm asset (cryptoProperties.assetType
12
+ * `algorithm`), first match wins. A hybrid combiner (primitive `combiner`) is
13
+ * not classified: IR 8547 section 3.2.1 approves a composite scheme with one
14
+ * approved input, so the OID of one input does not decide it.
15
+ *
16
+ * 1. `algorithmProperties.algorithmFamily`. A quantum-safe family (SHA-2,
17
+ * SHA-3, AES, ML-KEM, …) is safe, except its 112-bit members (SHA-224,
18
+ * SHA-512/224, SHA3-224), which are recognised by OID or name.
19
+ * 2. `cryptoProperties.oid` (rsaEncryption, id-ecPublicKey, the RSA, ECDSA,
20
+ * EdDSA and DSA signature OIDs, X25519/X448, DH, …).
21
+ * 3. The component name, lower case with only letters and digits kept
22
+ * ("SHA-512/224" is `sha512224`), then resolveAlgorithmFamily(name). A
23
+ * name that resolves to a hash family but names a DSA, RSA or ECDSA
24
+ * signature ("SHA256withDSA", "dsa-with-sha256", "RSA-SHA-512") is that
25
+ * signature algorithm, not its digest.
26
+ *
27
+ * A class whose members have a key family (rsa, ff, ec) is checked against
28
+ * the minimum key size of IR 8547 Tables 2 and 4 (112 bits of classical
29
+ * strength: RSA and finite-field 2048, elliptic curve 224). An algorithm with
30
+ * a known smaller key, and a public or private key asset
31
+ * (`related-crypto-material`) whose size is smaller, is classed
32
+ * `below-minimum-strength` instead. The key size of an algorithm comes from
33
+ * the first of these that gives a plausible key size (at least 512 for rsa
34
+ * and ff, 160 to 571 for ec): a trailing number in its name ("RSA-1024")
35
+ * unless it follows a hash token ("RSASSA-PSS-SHA-512",
36
+ * "sha512WithRSAEncryption-512"), an RFC 7919 group name ("ffdhe2048"), its
37
+ * `parameterSetIdentifier` (a digest size such as "256" on an RSA signature
38
+ * is not a key size) or the bit size in its curve name. A key asset is
39
+ * checked with its own size against the base class of its algorithm, so a
40
+ * 4096-bit key of an algorithm named "RSA-1024" is not reported again.
41
+ *
42
+ * The likelihood and impact levels are the fixed per-class rating the data
43
+ * file records. No confidence is emitted (the rating is not a measurement)
44
+ * and no organisational fact is invented: no owner, status or responses.
45
+ */
46
+
47
+ import { readFileSync } from "node:fs";
48
+ import { join } from "node:path";
49
+
50
+ import { dirNameStr } from "../core/paths.js";
51
+ import { resolveAlgorithmFamily } from "./cryptoAlgorithmFamily.js";
52
+
53
+ /** Prefix of every risk refId this module derives. */
54
+ export const PQC_RISK_REF_ID_PREFIX = "risk-pqc-";
55
+
56
+ const pqcData = JSON.parse(
57
+ readFileSync(join(dirNameStr, "data", "pqc-migration.json"), "utf-8"),
58
+ );
59
+
60
+ const QUANTUM_SAFE_FAMILIES = new Set(pqcData.quantumSafeFamilies);
61
+ const HASH_FAMILIES = new Set(["SHA-1", "SHA-2", "SHA-3"]);
62
+ const NAMED_GROUP_KEY_SIZES = new Map(
63
+ Object.entries(pqcData.namedGroupKeySizes).filter(
64
+ ([name]) => !name.startsWith("$"),
65
+ ),
66
+ );
67
+ const UNCLASSIFIED_PRIMITIVES = new Set(pqcData.unclassifiedPrimitives);
68
+ const BELOW_MINIMUM_CLASS = "below-minimum-strength";
69
+ const SYMMETRIC_112_CLASS = "symmetric-112-bit";
70
+ const KEY_MATERIAL_TYPES = new Set(["public-key", "private-key", "key"]);
71
+ const PLAUSIBLE_KEY_SIZES = Object.freeze({
72
+ rsa: [512, 65536],
73
+ ff: [512, 65536],
74
+ ec: [160, 571],
75
+ });
76
+
77
+ function buildLookup(member) {
78
+ const lookup = new Map();
79
+ for (const [className, definition] of Object.entries(pqcData.classes)) {
80
+ for (const [key, keyFamily] of Object.entries(definition[member] || {})) {
81
+ lookup.set(key, keyFamily ? { className, keyFamily } : { className });
82
+ }
83
+ }
84
+ return lookup;
85
+ }
86
+
87
+ const BY_FAMILY = buildLookup("families");
88
+ const BY_OID = buildLookup("oids");
89
+ const BY_NAME = buildLookup("names");
90
+
91
+ const isDigits = (text) =>
92
+ typeof text === "string" &&
93
+ text.length > 0 &&
94
+ [...text].every((ch) => ch >= "0" && ch <= "9");
95
+
96
+ /**
97
+ * A name in lower case with only ASCII letters and digits kept, the form the
98
+ * `names` tables of data/pqc-migration.json use.
99
+ *
100
+ * @param {string} name Algorithm or key name
101
+ * @returns {string} The compact name, empty for no name
102
+ */
103
+ export function compactAlgorithmName(name) {
104
+ let compact = "";
105
+ for (const ch of `${name ?? ""}`.toLowerCase()) {
106
+ if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9")) {
107
+ compact += ch;
108
+ }
109
+ }
110
+ return compact;
111
+ }
112
+
113
+ /** Is this name segment a digest name ("SHA", "sha512WithRSAEncryption")? */
114
+ function isHashToken(segment) {
115
+ const compact = compactAlgorithmName(segment);
116
+ return compact.startsWith("sha") || compact === "hash";
117
+ }
118
+
119
+ /**
120
+ * "RSA-2048" is `{base: "RSA", size: 2048}`; other names have no size. A
121
+ * trailing number after a hash token is a digest size, so
122
+ * "RSASSA-PSS-SHA-512" has no size.
123
+ */
124
+ function splitNamedKeySize(name) {
125
+ const text = `${name ?? ""}`;
126
+ const index = Math.max(text.lastIndexOf("-"), text.lastIndexOf("_"));
127
+ const tail = index > 0 ? text.slice(index + 1) : "";
128
+ if (!isDigits(tail) || tail.length > 5) {
129
+ return { base: text };
130
+ }
131
+ const head = text.slice(0, index);
132
+ const previous = head.slice(
133
+ Math.max(
134
+ head.lastIndexOf("-"),
135
+ head.lastIndexOf("_"),
136
+ head.lastIndexOf("/"),
137
+ ) + 1,
138
+ );
139
+ if (isHashToken(previous)) {
140
+ return { base: text };
141
+ }
142
+ return { base: head, size: Number(tail) };
143
+ }
144
+
145
+ const SIGNATURE_FAMILY_BY_TOKEN = Object.freeze([
146
+ ["ecdsa", "ECDSA"],
147
+ ["dsa", "DSA"],
148
+ ["rsa", "RSASSA-PKCS1"],
149
+ ]);
150
+
151
+ /**
152
+ * The signature family of a name that pairs a hash with DSA, RSA or ECDSA:
153
+ * "SHA256withDSA", "dsa-with-sha256", "RSA-SHA-512". ECDSA is checked before
154
+ * DSA, which it contains.
155
+ */
156
+ function signatureFamilyOfHashName(name) {
157
+ const compact = compactAlgorithmName(name);
158
+ const tokens = `${name ?? ""}`
159
+ .toLowerCase()
160
+ .split(/[-_/ ]/u)
161
+ .filter(Boolean);
162
+ const hasHashToken = tokens.some(isHashToken);
163
+ for (const [token, family] of SIGNATURE_FAMILY_BY_TOKEN) {
164
+ if (
165
+ compact.includes(`with${token}`) ||
166
+ compact.startsWith(`${token}with`) ||
167
+ (hasHashToken && tokens.includes(token))
168
+ ) {
169
+ return family;
170
+ }
171
+ }
172
+ return undefined;
173
+ }
174
+
175
+ function lookupCompactName(name) {
176
+ const compact = compactAlgorithmName(name);
177
+ return compact ? BY_NAME.get(compact) : undefined;
178
+ }
179
+
180
+ function lookupName(name) {
181
+ const { base } = splitNamedKeySize(name);
182
+ const byName = lookupCompactName(name) ?? lookupCompactName(base);
183
+ if (byName) {
184
+ return byName;
185
+ }
186
+ let family = resolveAlgorithmFamily(name) ?? resolveAlgorithmFamily(base);
187
+ if (!family || HASH_FAMILIES.has(family)) {
188
+ family = signatureFamilyOfHashName(name) ?? family;
189
+ }
190
+ return family ? BY_FAMILY.get(family) : undefined;
191
+ }
192
+
193
+ /** The first run of digits in a curve name ("secg/secp192r1" is 192). */
194
+ function curveBits(curve) {
195
+ const text = `${curve ?? ""}`;
196
+ let digits = "";
197
+ for (const ch of text.slice(text.lastIndexOf("/") + 1)) {
198
+ if (ch >= "0" && ch <= "9") {
199
+ digits += ch;
200
+ } else if (digits) {
201
+ break;
202
+ }
203
+ }
204
+ return digits && digits.length <= 4 ? Number(digits) : undefined;
205
+ }
206
+
207
+ function plausibleKeySize(size, keyFamily) {
208
+ const range = PLAUSIBLE_KEY_SIZES[keyFamily];
209
+ return range && size >= range[0] && size <= range[1] ? size : undefined;
210
+ }
211
+
212
+ /** The key size a name states: a trailing number or an RFC 7919 group. */
213
+ function namedKeySize(name) {
214
+ return (
215
+ splitNamedKeySize(name).size ??
216
+ NAMED_GROUP_KEY_SIZES.get(compactAlgorithmName(name))
217
+ );
218
+ }
219
+
220
+ function algorithmKeySize(component, keyFamily) {
221
+ const algorithm = component.cryptoProperties?.algorithmProperties || {};
222
+ const parameterSet = `${algorithm.parameterSetIdentifier ?? ""}`;
223
+ const candidates = [
224
+ namedKeySize(component.name),
225
+ isDigits(parameterSet) ? Number(parameterSet) : undefined,
226
+ keyFamily === "ec"
227
+ ? curveBits(algorithm.ellipticCurve ?? algorithm.curve)
228
+ : undefined,
229
+ ];
230
+ for (const candidate of candidates) {
231
+ const size =
232
+ candidate === undefined
233
+ ? undefined
234
+ : plausibleKeySize(candidate, keyFamily);
235
+ if (size !== undefined) {
236
+ return size;
237
+ }
238
+ }
239
+ return undefined;
240
+ }
241
+
242
+ /** Apply the IR 8547 minimum key size to a key-family classification. */
243
+ function withStrengthCheck(match, keySize) {
244
+ const minimumKeySize = pqcData.minimumKeySizes[match.keyFamily];
245
+ if (keySize !== undefined && minimumKeySize && keySize < minimumKeySize) {
246
+ return {
247
+ className: BELOW_MINIMUM_CLASS,
248
+ keyFamily: match.keyFamily,
249
+ keySize,
250
+ minimumKeySize,
251
+ };
252
+ }
253
+ return { ...match };
254
+ }
255
+
256
+ /**
257
+ * The class of an algorithm asset before the minimum key size is applied, or
258
+ * undefined when it is quantum-safe or unclassified.
259
+ */
260
+ function baseAlgorithmClass(component) {
261
+ const cryptoProperties = component.cryptoProperties;
262
+ const family = cryptoProperties.algorithmProperties?.algorithmFamily;
263
+ if (
264
+ UNCLASSIFIED_PRIMITIVES.has(cryptoProperties.algorithmProperties?.primitive)
265
+ ) {
266
+ return undefined;
267
+ }
268
+ if (family && QUANTUM_SAFE_FAMILIES.has(family)) {
269
+ const refined =
270
+ BY_OID.get(cryptoProperties.oid) ?? lookupCompactName(component.name);
271
+ return refined?.className === SYMMETRIC_112_CLASS
272
+ ? { ...refined }
273
+ : undefined;
274
+ }
275
+ const match =
276
+ (family && BY_FAMILY.get(family)) ||
277
+ BY_OID.get(cryptoProperties.oid) ||
278
+ lookupName(component.name);
279
+ return match ? { ...match } : undefined;
280
+ }
281
+
282
+ function classifyAlgorithm(component) {
283
+ const match = baseAlgorithmClass(component);
284
+ if (!match?.keyFamily) {
285
+ return match;
286
+ }
287
+ return withStrengthCheck(match, algorithmKeySize(component, match.keyFamily));
288
+ }
289
+
290
+ function classifyKey(component, algorithmsByRef) {
291
+ const material = component.cryptoProperties.relatedCryptoMaterialProperties;
292
+ if (!KEY_MATERIAL_TYPES.has(material?.type)) {
293
+ return undefined;
294
+ }
295
+ const algorithm = algorithmsByRef.get(material.algorithmRef);
296
+ const match = algorithm?.cryptoProperties
297
+ ? baseAlgorithmClass(algorithm)
298
+ : lookupName(component.name);
299
+ const keySize =
300
+ typeof material.size === "number"
301
+ ? material.size
302
+ : namedKeySize(component.name);
303
+ if (!match?.keyFamily || keySize === undefined) {
304
+ return undefined;
305
+ }
306
+ const checked = withStrengthCheck(match, keySize);
307
+ return checked.className === BELOW_MINIMUM_CLASS ? checked : undefined;
308
+ }
309
+
310
+ /**
311
+ * Classify one cryptographic-asset component. Algorithm assets get their
312
+ * IR 8547 class; key assets are classified only when their size is below the
313
+ * IR 8547 minimum (the algorithm asset carries the quantum risk).
314
+ *
315
+ * @param {object} component A component with cryptoProperties
316
+ * @param {Map<string, object>} [algorithmsByRef] Components by bom-ref, to
317
+ * resolve a key's `algorithmRef`
318
+ * @returns {{className: string, keyFamily?: string, keySize?: number, minimumKeySize?: number}|undefined}
319
+ * The class, or undefined when the asset is quantum-safe or unclassified
320
+ */
321
+ export function classifyPqcCryptoAsset(component, algorithmsByRef = new Map()) {
322
+ const assetType = component?.cryptoProperties?.assetType;
323
+ if (assetType === "algorithm") {
324
+ return classifyAlgorithm(component);
325
+ }
326
+ if (assetType === "related-crypto-material") {
327
+ return classifyKey(component, algorithmsByRef);
328
+ }
329
+ return undefined;
330
+ }
331
+
332
+ function flattenComponents(components, into = []) {
333
+ for (const component of Array.isArray(components) ? components : []) {
334
+ if (component && typeof component === "object") {
335
+ into.push(component);
336
+ flattenComponents(component.components, into);
337
+ }
338
+ }
339
+ return into;
340
+ }
341
+
342
+ /**
343
+ * Build the risks.risks[] entries for the quantum-vulnerable and weak
344
+ * cryptographic assets of a 1.x-shaped internal BOM: one per asset with a
345
+ * bom-ref (an asset without one could not be referenced from `affects`), in
346
+ * component order, unique by refId.
347
+ *
348
+ * @param {object} internalBom The internal model
349
+ * @returns {object[]} Risk entries shaped for the draft's
350
+ * #/$defs/cyclonedx-risk-2.0/$defs/risk; empty when nothing is at risk
351
+ */
352
+ export function collectPqcRisks(internalBom) {
353
+ const components = flattenComponents(internalBom?.components);
354
+ const byRef = new Map();
355
+ for (const component of components) {
356
+ if (typeof component["bom-ref"] === "string") {
357
+ byRef.set(component["bom-ref"], component);
358
+ }
359
+ }
360
+ const risks = [];
361
+ const seen = new Set();
362
+ for (const component of components) {
363
+ const bomRef = component["bom-ref"];
364
+ if (typeof bomRef !== "string" || !bomRef.length) {
365
+ continue;
366
+ }
367
+ const classification = classifyPqcCryptoAsset(component, byRef);
368
+ const refId = `${PQC_RISK_REF_ID_PREFIX}${bomRef}`;
369
+ if (classification && !seen.has(refId)) {
370
+ seen.add(refId);
371
+ risks.push(buildRiskEntry(component, classification, refId));
372
+ }
373
+ }
374
+ return risks;
375
+ }
376
+
377
+ const fillTemplate = (template, values) =>
378
+ Object.entries(values).reduce(
379
+ (text, [key, value]) => text.split(`{${key}}`).join(`${value}`),
380
+ template,
381
+ );
382
+
383
+ const PROPERTY_FIELDS = Object.freeze([
384
+ ["deprecatedAfter", "cdx:crypto:pqc:deprecatedAfter"],
385
+ ["disallowedAfter", "cdx:crypto:pqc:disallowedAfter"],
386
+ ["disallowedIn", "cdx:crypto:pqc:disallowedIn"],
387
+ ["transition", "cdx:crypto:pqc:transition"],
388
+ ["source", "cdx:crypto:pqc:source"],
389
+ ]);
390
+
391
+ function buildRiskEntry(component, classification, refId) {
392
+ const definition = pqcData.classes[classification.className];
393
+ const label =
394
+ typeof component.name === "string" && component.name.length
395
+ ? component.name
396
+ : component.cryptoProperties?.algorithmProperties?.algorithmFamily ||
397
+ "algorithm";
398
+ const properties = [
399
+ { name: "cdx:crypto:pqc:class", value: classification.className },
400
+ ];
401
+ for (const [field, name] of PROPERTY_FIELDS) {
402
+ if (definition[field]) {
403
+ properties.push({ name, value: definition[field] });
404
+ }
405
+ }
406
+ return {
407
+ refId,
408
+ name: `${definition.riskName}: ${label}`,
409
+ statement: definition.statement,
410
+ domains: [{ type: "security" }],
411
+ affects: [component["bom-ref"]],
412
+ inherentRisk: {
413
+ likelihood: { level: definition.likelihood },
414
+ impacts: [{ level: definition.impact }],
415
+ rationale: fillTemplate(definition.rationale, {
416
+ algorithm: label,
417
+ size: classification.keySize,
418
+ minimum: classification.minimumKeySize,
419
+ }),
420
+ },
421
+ properties,
422
+ externalReferences: [
423
+ { type: "documentation", url: definition.url || pqcData.source.url },
424
+ ],
425
+ };
426
+ }
@@ -3,6 +3,7 @@ import { basename, dirname, join } from "node:path";
3
3
 
4
4
  import { safeExistsSync } from "../core/fs.js";
5
5
  import { parseToml } from "../core/toml.js";
6
+ import { formulationSourcePath } from "./ecosystemFormulationParsers.js";
6
7
 
7
8
  // Keep this list conservative and high-signal: it is meant to surface common
8
9
  // Rust native build helpers for audit triage, not to exhaustively model every
@@ -53,17 +54,18 @@ function appendProperty(properties, name, value) {
53
54
  });
54
55
  }
55
56
 
56
- function createFormulationComponent(name, filePath, toolName) {
57
+ function createFormulationComponent(name, filePath, toolName, rootDir) {
58
+ const sourcePath = formulationSourcePath(filePath, rootDir);
57
59
  return {
58
60
  type: "application",
59
61
  name,
60
62
  version: "config",
61
- "bom-ref": `urn:cdxgen:formulation:${toolName}:${filePath}`,
62
- properties: [{ name: "internal:SrcFile", value: filePath }],
63
+ "bom-ref": `urn:cdxgen:formulation:${toolName}:${sourcePath}`,
64
+ properties: [{ name: "internal:SrcFile", value: sourcePath }],
63
65
  };
64
66
  }
65
67
 
66
- function parseCargoManifest(filePath) {
68
+ function parseCargoManifest(filePath, rootDir) {
67
69
  let cargoData;
68
70
  try {
69
71
  cargoData = parseToml(readFileSync(filePath, { encoding: "utf-8" }));
@@ -75,7 +77,12 @@ function parseCargoManifest(filePath) {
75
77
  }
76
78
  const packageName =
77
79
  cargoData?.package?.name || basename(dirname(filePath)) || "cargo-project";
78
- const component = createFormulationComponent(packageName, filePath, "cargo");
80
+ const component = createFormulationComponent(
81
+ packageName,
82
+ filePath,
83
+ "cargo",
84
+ rootDir,
85
+ );
79
86
  const properties = component.properties;
80
87
  const buildDependencies = cargoData?.["build-dependencies"];
81
88
  const devDependencies = cargoData?.["dev-dependencies"];
@@ -248,7 +255,7 @@ function parseCargoManifest(filePath) {
248
255
  return component;
249
256
  }
250
257
 
251
- function parsePyProject(filePath) {
258
+ function parsePyProject(filePath, rootDir) {
252
259
  let pyprojectData;
253
260
  try {
254
261
  pyprojectData = parseToml(readFileSync(filePath, { encoding: "utf-8" }));
@@ -269,6 +276,7 @@ function parsePyProject(filePath) {
269
276
  "maturin-project",
270
277
  filePath,
271
278
  "maturin",
279
+ rootDir,
272
280
  );
273
281
  const properties = component.properties;
274
282
  appendProperty(properties, "cdx:rust:buildTool", "maturin");
@@ -311,23 +319,26 @@ function parsePyProject(filePath) {
311
319
  * formulation components carrying build-tool, build-script capability, native
312
320
  * build dependency, and maturin metadata properties.
313
321
  *
314
- * @type {{id: string, patterns: string[], parse(files: string[]): {components: Object[]}}}
322
+ * Components record the manifest path relative to context.rootDir, the
323
+ * scanned project root.
324
+ *
325
+ * @type {{id: string, patterns: string[], parse(files: string[], options?: Object, context?: {rootDir?: string}): {components: Object[]}}}
315
326
  */
316
327
  export const rustFormulationParser = {
317
328
  id: "rust-build",
318
329
  patterns: ["**/Cargo.toml", "**/pyproject.toml"],
319
- parse(files) {
330
+ parse(files, _options, context = {}) {
320
331
  const components = [];
321
332
  for (const filePath of files || []) {
322
333
  if (filePath.endsWith("Cargo.toml")) {
323
- const cargoComponent = parseCargoManifest(filePath);
334
+ const cargoComponent = parseCargoManifest(filePath, context.rootDir);
324
335
  if (cargoComponent) {
325
336
  components.push(cargoComponent);
326
337
  }
327
338
  continue;
328
339
  }
329
340
  if (filePath.endsWith("pyproject.toml")) {
330
- const pyprojectComponent = parsePyProject(filePath);
341
+ const pyprojectComponent = parsePyProject(filePath, context.rootDir);
331
342
  if (pyprojectComponent) {
332
343
  components.push(pyprojectComponent);
333
344
  }
@@ -106,9 +106,48 @@ function inferGitOperation(args = []) {
106
106
  return "command";
107
107
  }
108
108
 
109
+ let gitRedirectRefusalReported = false;
110
+
111
+ registerRunReset("source:git-redirect-refusal", () => {
112
+ gitRedirectRefusalReported = false;
113
+ });
114
+
115
+ /**
116
+ * Determine whether git must refuse HTTP redirects. A git host allowlist is
117
+ * checked against the URL cdxgen is given, and git would otherwise follow
118
+ * the server's redirect on its first request to a host the allowlist never
119
+ * saw. Secure mode refuses redirects in the HTTP client too.
120
+ *
121
+ * @returns {boolean} true when a git host allowlist or secure mode applies
122
+ */
123
+ export function gitRedirectsRefused() {
124
+ return isSecureMode || getAllowedHosts().length > 0;
125
+ }
126
+
127
+ /**
128
+ * Determine whether a failed git command stopped at an HTTP redirect.
129
+ *
130
+ * @param {string|undefined} stderr git error output
131
+ * @returns {boolean} true when git reports a 3xx response
132
+ */
133
+ function isGitRedirectRefusal(stderr) {
134
+ if (typeof stderr !== "string") {
135
+ return false;
136
+ }
137
+ const marker = "The requested URL returned error: 30";
138
+ const index = stderr.indexOf(marker);
139
+ return index !== -1 && "12378".includes(stderr[index + marker.length]);
140
+ }
141
+
109
142
  /**
110
143
  * Execute git with hardened defaults.
111
144
  *
145
+ * Every call runs with command-scope settings that keep the repository from
146
+ * running fsmonitor hooks or being opened as a bare repository. While a git
147
+ * host allowlist or secure mode applies, git also refuses HTTP redirects, so
148
+ * an allowed host cannot send a clone or ls-remote to a host the allowlist
149
+ * does not name.
150
+ *
112
151
  * @param {string[]} args git arguments
113
152
  * @param {Object} options command options
114
153
  * @param {string|undefined} options.cwd working directory
@@ -120,15 +159,23 @@ function inferGitOperation(args = []) {
120
159
  export function hardenedGitCommand(args, options = {}) {
121
160
  const gitOperation = inferGitOperation(args);
122
161
  const gitAllowProtocol = getGitAllowProtocol();
162
+ const refuseRedirects = gitRedirectsRefused();
163
+ const commandConfig = [
164
+ ["core.fsmonitor", "false"],
165
+ ["safe.bareRepository", "explicit"],
166
+ ];
167
+ if (refuseRedirects) {
168
+ commandConfig.push(["http.followRedirects", "false"]);
169
+ }
123
170
  const envConfigs = {
124
- GIT_CONFIG_COUNT: "2",
125
- GIT_CONFIG_KEY_0: "core.fsmonitor",
126
- GIT_CONFIG_VALUE_0: "false",
127
- GIT_CONFIG_KEY_1: "safe.bareRepository",
128
- GIT_CONFIG_VALUE_1: "explicit",
171
+ GIT_CONFIG_COUNT: String(commandConfig.length),
129
172
  GIT_NO_LAZY_FETCH: "1",
130
173
  GIT_TERMINAL_PROMPT: "0",
131
174
  };
175
+ commandConfig.forEach(([key, value], index) => {
176
+ envConfigs[`GIT_CONFIG_KEY_${index}`] = key;
177
+ envConfigs[`GIT_CONFIG_VALUE_${index}`] = value;
178
+ });
132
179
  if (options.noRepository) {
133
180
  // ls-remote works without a repository. Pointing GIT_DIR at a path that
134
181
  // does not exist stops git from discovering one in the working directory.
@@ -147,7 +194,7 @@ export function hardenedGitCommand(args, options = {}) {
147
194
  ...envConfigs,
148
195
  GIT_ALLOW_PROTOCOL: gitAllowProtocol,
149
196
  };
150
- return safeSpawnSync("git", args, {
197
+ const result = safeSpawnSync("git", args, {
151
198
  cdxgenActivity: {
152
199
  blockedReason: `Dry run mode blocks git ${gitOperation} operations.`,
153
200
  gitOperation,
@@ -160,6 +207,18 @@ export function hardenedGitCommand(args, options = {}) {
160
207
  cwd: options.cwd,
161
208
  env,
162
209
  });
210
+ if (
211
+ refuseRedirects &&
212
+ !gitRedirectRefusalReported &&
213
+ result?.status !== 0 &&
214
+ isGitRedirectRefusal(result?.stderr)
215
+ ) {
216
+ gitRedirectRefusalReported = true;
217
+ console.warn(
218
+ "A Git server answered with a redirect, which cdxgen does not follow while CDXGEN_GIT_ALLOWED_HOSTS, CDXGEN_SERVER_ALLOWED_HOSTS or secure mode is in effect. Use the repository's current URL instead.",
219
+ );
220
+ }
221
+ return result;
163
222
  }
164
223
 
165
224
  function normalizeTagName(tagName) {
@@ -15,6 +15,7 @@ import {
15
15
  normalizeHuggingFaceReference,
16
16
  repositoryUrlForHuggingFaceAssetType,
17
17
  toHuggingFacePurl,
18
+ toHuggingFaceSupplier,
18
19
  } from "./huggingfaceUtils.js";
19
20
 
20
21
  /**
@@ -278,7 +279,7 @@ export function repoIdFromFixtureDirectory(filePath) {
278
279
  *
279
280
  * @param {string} modelRef model, dataset, or space reference
280
281
  * @param {{ includeDatasetPurl?: boolean }} [options={}] pedigree reference options
281
- * @returns {{ "bom-ref": string, group: string, name: string, purl?: string, type: string }|undefined} component reference
282
+ * @returns {{ "bom-ref": string, group: string, name: string, purl?: string, supplier?: { name: string, url?: string[] }, type: string }|undefined} component reference
282
283
  */
283
284
  export function createHuggingFaceComponentReference(modelRef, _options = {}) {
284
285
  const reference = normalizeHuggingFaceReference(modelRef);
@@ -302,6 +303,9 @@ export function createHuggingFaceComponentReference(modelRef, _options = {}) {
302
303
  group,
303
304
  name,
304
305
  purl,
306
+ // The namespace doubles as the producer for references, so pedigree
307
+ // ancestors and Space-linked models carry the producer element too.
308
+ supplier: toHuggingFaceSupplier(group),
305
309
  };
306
310
  }
307
311
 
@@ -329,6 +333,7 @@ export function createHuggingFaceComponentReference(modelRef, _options = {}) {
329
333
  * properties?: Array<object>,
330
334
  * purl?: string,
331
335
  * scope?: string,
336
+ * supplier?: { name: string, url?: string[] },
332
337
  * tags?: string[],
333
338
  * type: "data",
334
339
  * },
@@ -369,6 +374,9 @@ export function createHuggingFaceDatasetReference(dataset, options = {}) {
369
374
  scope: options.componentScope,
370
375
  description: normalizedDataset.description,
371
376
  externalReferences,
377
+ // The dataset's namespace is the creator identifier the provenance
378
+ // element asks for, when the reference carries one.
379
+ supplier: toHuggingFaceSupplier(normalizedDataset.group),
372
380
  data: [toDatasetComponentData(normalizedDataset)],
373
381
  properties: options.componentProperties,
374
382
  tags: options.componentTags,
@@ -195,6 +195,24 @@ export function toHuggingFaceAssetUrl(assetType, repoId) {
195
195
  return assetPath ? `${HF_BASE_URL}/${assetPath}` : undefined;
196
196
  }
197
197
 
198
+ /**
199
+ * Build the supplier entity for a Hugging Face namespace: the namespace as
200
+ * the name and its organisation or user page as the url. The url is left
201
+ * out when the namespace has characters a Hub account name cannot have.
202
+ *
203
+ * @param {string} namespace Hugging Face organisation or user name
204
+ * @returns {{ name: string, url?: string[] }|undefined} supplier entity
205
+ */
206
+ export function toHuggingFaceSupplier(namespace) {
207
+ const name = typeof namespace === "string" ? namespace.trim() : "";
208
+ if (!name) {
209
+ return undefined;
210
+ }
211
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)
212
+ ? { name, url: [`${HF_BASE_URL}/${name}`] }
213
+ : { name };
214
+ }
215
+
198
216
  /**
199
217
  * Convert a Hugging Face repo reference to a package URL.
200
218
  *