@cdxgen/cdxgen 13.0.1 → 13.2.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 (286) hide show
  1. package/README.md +30 -16
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +141 -7
  4. package/bin/evinse.js +3 -1
  5. package/data/component-tags.json +8 -1
  6. package/data/crypto-oid.json +88 -12
  7. package/data/frameworks-list.json +86 -1
  8. package/data/remediations.json +1167 -0
  9. package/data/rules/ai-bom.yaml +78 -0
  10. package/data/rules/asar-archives.yaml +1 -1
  11. package/data/rules/build-fidelity.yaml +782 -0
  12. package/data/rules/cargo-rust.yaml +132 -0
  13. package/data/rules/mcp-servers.yaml +122 -0
  14. package/data/rules/package-integrity.yaml +13 -13
  15. package/lib/audit/index.js +48 -4
  16. package/lib/audit/scoring.js +2 -8
  17. package/lib/audit/targets.js +6 -1
  18. package/lib/cli/bomAssembly.js +32 -1
  19. package/lib/cli/cliOptions.js +94 -7
  20. package/lib/cli/index.js +268 -49
  21. package/lib/cli/jsBom.js +91 -5
  22. package/lib/cli/jvmBom.js +1515 -1314
  23. package/lib/cli/managedBom.js +249 -28
  24. package/lib/cli/nativeBom.js +986 -10
  25. package/lib/core/activity.js +120 -2
  26. package/lib/core/buildLedger.js +799 -0
  27. package/lib/core/deferredExit.js +141 -0
  28. package/lib/core/env.js +108 -2
  29. package/lib/core/fs.js +60 -0
  30. package/lib/core/logger.js +12 -99
  31. package/lib/core/severity.js +16 -0
  32. package/lib/core/syncFileWriter.js +106 -0
  33. package/lib/core/userAgent.js +151 -0
  34. package/lib/ecosystems/cargoCache.js +364 -0
  35. package/lib/ecosystems/caxa.js +13 -5
  36. package/lib/ecosystems/cppEvidence.js +40 -2
  37. package/lib/ecosystems/ecosystems.js +250 -91
  38. package/lib/ecosystems/golem.js +11 -1
  39. package/lib/ecosystems/gradleutils.js +498 -17
  40. package/lib/ecosystems/kosi.js +1256 -0
  41. package/lib/ecosystems/parsers-crystal.js +179 -0
  42. package/lib/ecosystems/parsers-dotnet.js +69 -39
  43. package/lib/ecosystems/parsers-elm.js +816 -0
  44. package/lib/ecosystems/parsers-gleam.js +1 -1
  45. package/lib/ecosystems/parsers-js.js +26 -0
  46. package/lib/ecosystems/parsers-julia.js +268 -0
  47. package/lib/ecosystems/parsers-jvm.js +9 -0
  48. package/lib/ecosystems/parsers-lua.js +218 -0
  49. package/lib/ecosystems/parsers-misc.js +188 -2
  50. package/lib/ecosystems/parsers-nim.js +221 -0
  51. package/lib/ecosystems/parsers-opam.js +180 -0
  52. package/lib/ecosystems/parsers-perl.js +277 -0
  53. package/lib/ecosystems/parsers-python.js +172 -36
  54. package/lib/ecosystems/parsers-r.js +148 -0
  55. package/lib/ecosystems/parsers-rebar.js +411 -0
  56. package/lib/ecosystems/parsers-rust.js +944 -19
  57. package/lib/ecosystems/parsers-spack.js +204 -0
  58. package/lib/ecosystems/parsers-stack.js +264 -0
  59. package/lib/ecosystems/parsers-terraform.js +194 -0
  60. package/lib/ecosystems/pypiClassifiers.js +326 -0
  61. package/lib/ecosystems/pythonutils.js +49 -1
  62. package/lib/ecosystems/rubyutils.js +13 -0
  63. package/lib/ecosystems/rusi.js +126 -6
  64. package/lib/evinser/evinser.js +141 -13
  65. package/lib/helpers/agenticToolutils.js +925 -0
  66. package/lib/helpers/remote/dependency-track.js +81 -14
  67. package/lib/inventory/agentFormulationParser.js +57 -4
  68. package/lib/inventory/aiCollector.js +504 -10
  69. package/lib/inventory/aiInventory.js +12 -0
  70. package/lib/inventory/analyzer.js +488 -30
  71. package/lib/inventory/astWorker.js +6 -0
  72. package/lib/inventory/auditCategories.js +17 -0
  73. package/lib/inventory/cbomutils.js +30 -0
  74. package/lib/inventory/cdxrs.js +7 -2
  75. package/lib/inventory/communityAiConfigParser.js +215 -2
  76. package/lib/inventory/depsUtils.js +72 -0
  77. package/lib/inventory/dosai.js +508 -22
  78. package/lib/inventory/envcontext.js +215 -24
  79. package/lib/inventory/evidenceUtils.js +13 -0
  80. package/lib/inventory/fetchBatch.js +131 -2
  81. package/lib/inventory/fetchRate.js +74 -11
  82. package/lib/inventory/forgeEnricher.js +3 -2
  83. package/lib/inventory/jvmToolEnv.js +594 -0
  84. package/lib/inventory/mcpConfigParser.js +166 -2
  85. package/lib/inventory/mcpDiscovery.js +11 -0
  86. package/lib/inventory/mcpServerManifest.js +389 -0
  87. package/lib/inventory/mcpbBundle.js +419 -0
  88. package/lib/inventory/osinfo.js +1 -0
  89. package/lib/inventory/plugins.js +3 -0
  90. package/lib/inventory/protobom.js +54 -88
  91. package/lib/inventory/protobomLoader.js +4 -0
  92. package/lib/inventory/rustFormulationParser.js +1 -1
  93. package/lib/inventory/source.js +10 -4
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/toolRequirements.js +903 -0
  97. package/lib/managers/binary.js +24 -0
  98. package/lib/managers/docker.js +26 -5
  99. package/lib/parsers/huggingfaceUtils.js +15 -7
  100. package/lib/parsers/onnx.js +294 -0
  101. package/lib/parsers/safetensors.js +114 -0
  102. package/lib/server/server.js +1 -1
  103. package/lib/stages/postgen/annotationBuilder.js +97 -0
  104. package/lib/stages/postgen/auditBom.js +76 -79
  105. package/lib/stages/postgen/introspection/annotate.js +320 -0
  106. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  107. package/lib/stages/postgen/introspection/emit.js +192 -0
  108. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  109. package/lib/stages/postgen/introspection/markers.js +250 -0
  110. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  111. package/lib/stages/postgen/introspection/report.js +1208 -0
  112. package/lib/stages/postgen/introspection/score.js +1067 -0
  113. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  114. package/lib/stages/postgen/parentEdge.js +156 -0
  115. package/lib/stages/postgen/postgen.js +284 -3
  116. package/lib/stages/postgen/ruleEngine.js +17 -0
  117. package/lib/stages/pregen/envAudit.js +42 -2
  118. package/lib/stages/pregen/pregen.js +1171 -6
  119. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  120. package/lib/validator/bomValidator.js +11 -2
  121. package/package.json +26 -26
  122. package/types/lib/audit/index.d.ts.map +1 -1
  123. package/types/lib/audit/scoring.d.ts +2 -8
  124. package/types/lib/audit/scoring.d.ts.map +1 -1
  125. package/types/lib/audit/targets.d.ts.map +1 -1
  126. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  127. package/types/lib/cli/cliOptions.d.ts +2 -1
  128. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  129. package/types/lib/cli/index.d.ts +10 -2
  130. package/types/lib/cli/index.d.ts.map +1 -1
  131. package/types/lib/cli/jsBom.d.ts +23 -0
  132. package/types/lib/cli/jsBom.d.ts.map +1 -1
  133. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  134. package/types/lib/cli/managedBom.d.ts +33 -0
  135. package/types/lib/cli/managedBom.d.ts.map +1 -1
  136. package/types/lib/cli/nativeBom.d.ts +88 -8
  137. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  138. package/types/lib/core/activity.d.ts.map +1 -1
  139. package/types/lib/core/buildLedger.d.ts +272 -0
  140. package/types/lib/core/buildLedger.d.ts.map +1 -0
  141. package/types/lib/core/deferredExit.d.ts +84 -0
  142. package/types/lib/core/deferredExit.d.ts.map +1 -0
  143. package/types/lib/core/env.d.ts +43 -1
  144. package/types/lib/core/env.d.ts.map +1 -1
  145. package/types/lib/core/fs.d.ts +21 -0
  146. package/types/lib/core/fs.d.ts.map +1 -1
  147. package/types/lib/core/logger.d.ts +2 -11
  148. package/types/lib/core/logger.d.ts.map +1 -1
  149. package/types/lib/core/severity.d.ts +11 -0
  150. package/types/lib/core/severity.d.ts.map +1 -0
  151. package/types/lib/core/syncFileWriter.d.ts +33 -0
  152. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  153. package/types/lib/core/userAgent.d.ts +56 -0
  154. package/types/lib/core/userAgent.d.ts.map +1 -0
  155. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  156. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  157. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  159. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  160. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  161. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  162. package/types/lib/ecosystems/gradleutils.d.ts +53 -0
  163. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  164. package/types/lib/ecosystems/kosi.d.ts +73 -0
  165. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  166. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  167. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  168. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  169. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  170. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  171. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  172. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  173. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  174. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  176. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  177. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  178. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  179. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  180. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  181. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  182. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  183. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  184. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  185. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  188. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  189. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  190. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  191. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  192. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  193. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  194. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  195. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  196. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  197. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  198. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  199. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  200. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  201. package/types/lib/ecosystems/pythonutils.d.ts +21 -1
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  205. package/types/lib/evinser/evinser.d.ts +34 -1
  206. package/types/lib/evinser/evinser.d.ts.map +1 -1
  207. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  208. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  209. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  210. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  211. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  212. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  213. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  214. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +12 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  219. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  220. package/types/lib/inventory/dosai.d.ts +43 -0
  221. package/types/lib/inventory/dosai.d.ts.map +1 -1
  222. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  223. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  224. package/types/lib/inventory/fetchBatch.d.ts +43 -0
  225. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  226. package/types/lib/inventory/fetchRate.d.ts +9 -6
  227. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  228. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  229. package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
  230. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  231. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  232. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  233. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  234. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  235. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  236. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  237. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  238. package/types/lib/inventory/plugins.d.ts.map +1 -1
  239. package/types/lib/inventory/protobom.d.ts +18 -13
  240. package/types/lib/inventory/protobom.d.ts.map +1 -1
  241. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  242. package/types/lib/inventory/source.d.ts.map +1 -1
  243. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  244. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  245. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  246. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  247. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  248. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  249. package/types/lib/managers/binary.d.ts.map +1 -1
  250. package/types/lib/managers/docker.d.ts.map +1 -1
  251. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  252. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  253. package/types/lib/parsers/onnx.d.ts +28 -0
  254. package/types/lib/parsers/onnx.d.ts.map +1 -0
  255. package/types/lib/parsers/safetensors.d.ts +22 -0
  256. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  257. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  258. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  259. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  260. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  261. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  262. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  263. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  264. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  265. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  266. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  267. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  268. package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
  269. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  270. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  271. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  272. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  273. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  275. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  276. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  277. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  278. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  279. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  280. package/types/lib/stages/postgen/postgen.d.ts +35 -2
  281. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  282. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  283. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  284. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  285. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  286. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -0,0 +1,204 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { tryBuildPurl } from "../inventory/purl.js";
4
+
5
+ /**
6
+ * HPC Spack parser for `spack.lock`.
7
+ *
8
+ * Spack writes a JSON lock file when an environment is concretised:
9
+ *
10
+ * - `roots` — the specs the user asked for, each as `{hash, spec}`.
11
+ * - `concrete_specs` — every node of the resolved DAG, keyed by its DAG
12
+ * hash. A node carries its `name`, `version`, `namespace`, the compiler
13
+ * and architecture it was built for, and a `dependencies` list whose
14
+ * entries reference other nodes by hash.
15
+ *
16
+ * The DAG hash is the identity Spack itself uses: two builds of the same
17
+ * version with different variants or compilers are different hashes, so the
18
+ * graph is keyed by hash throughout and the hash is carried into each
19
+ * component's bom-ref.
20
+ *
21
+ * No `spack` purl type is registered, so nodes are identified as generic
22
+ * packages carrying a `cdx:purl:proposedType=spack` property, following the
23
+ * convention used for nix and zig. The DAG hash is kept as a property because
24
+ * it identifies the concrete build, not the artifact bytes.
25
+ */
26
+
27
+ /**
28
+ * Parse a `spack.lock` file.
29
+ *
30
+ * @param {string} spackLockFile Path to `spack.lock`
31
+ * @returns {{ pkgList: object[], dependencies: object[], rootInputs: string[] }}
32
+ */
33
+ export function parseSpackLock(spackLockFile) {
34
+ let lock;
35
+ try {
36
+ lock = JSON.parse(readFileSync(spackLockFile, "utf-8"));
37
+ } catch (error) {
38
+ console.warn(`Failed to parse ${spackLockFile}: ${error.message}`);
39
+ return emptyResult();
40
+ }
41
+ const concreteSpecs =
42
+ lock?.concrete_specs && typeof lock.concrete_specs === "object"
43
+ ? lock.concrete_specs
44
+ : undefined;
45
+ if (!concreteSpecs) {
46
+ return emptyResult();
47
+ }
48
+
49
+ const pkgList = [];
50
+ const refByHash = new Map();
51
+ for (const [hash, node] of Object.entries(concreteSpecs)) {
52
+ if (typeof node?.name !== "string" || !node.name) {
53
+ continue;
54
+ }
55
+ const pkg = spackPackage(node.name, stringOrUndefined(node.version), {
56
+ hash,
57
+ namespace: stringOrUndefined(node.namespace),
58
+ arch: architectureLabel(node.arch),
59
+ compiler: compilerLabel(node.compiler),
60
+ srcFile: spackLockFile,
61
+ });
62
+ pkgList.push(pkg);
63
+ refByHash.set(hash, pkg["bom-ref"]);
64
+ }
65
+
66
+ const dependencies = [];
67
+ for (const [hash, node] of Object.entries(concreteSpecs)) {
68
+ const ref = refByHash.get(hash);
69
+ if (!ref) {
70
+ continue;
71
+ }
72
+ // Link and run edges are the ones that survive into the installed DAG;
73
+ // build-only edges are dropped to keep the graph at runtime relevance.
74
+ const dependsOn = new Set();
75
+ for (const dep of Array.isArray(node.dependencies)
76
+ ? node.dependencies
77
+ : []) {
78
+ const types = Array.isArray(dep?.type) ? dep.type : [];
79
+ if (!types.includes("link") && !types.includes("run")) {
80
+ continue;
81
+ }
82
+ const targetRef = refByHash.get(dep?.hash);
83
+ if (targetRef && targetRef !== ref) {
84
+ dependsOn.add(targetRef);
85
+ }
86
+ }
87
+ dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
88
+ }
89
+
90
+ // The environment names its own roots; nothing has to be inferred.
91
+ const rootInputs = (Array.isArray(lock.roots) ? lock.roots : [])
92
+ .map((root) => refByHash.get(root?.hash))
93
+ .filter(Boolean);
94
+ return { pkgList, dependencies, rootInputs: [...new Set(rootInputs)] };
95
+ }
96
+
97
+ /**
98
+ * Build a component-like package record for a Spack node.
99
+ *
100
+ * @param {string} name Node name
101
+ * @param {string|undefined} version Node version
102
+ * @param {object} opts Extra context (`hash`, `namespace`, `arch`, `compiler`, `srcFile`)
103
+ * @returns {object} Package record
104
+ */
105
+ function spackPackage(name, version, opts) {
106
+ const purl = tryBuildPurl({
107
+ type: "generic",
108
+ namespace: "spack",
109
+ name,
110
+ version: version || undefined,
111
+ });
112
+ const properties = [
113
+ { name: "internal:SrcFile", value: opts.srcFile },
114
+ { name: "cdx:purl:proposedType", value: "spack" },
115
+ ];
116
+ if (opts.hash) {
117
+ properties.push({ name: "cdx:spack:hash", value: opts.hash });
118
+ }
119
+ if (opts.namespace) {
120
+ properties.push({ name: "cdx:spack:namespace", value: opts.namespace });
121
+ }
122
+ if (opts.arch) {
123
+ properties.push({ name: "cdx:spack:arch", value: opts.arch });
124
+ }
125
+ if (opts.compiler) {
126
+ properties.push({ name: "cdx:spack:compiler", value: opts.compiler });
127
+ }
128
+ const pkg = {
129
+ name,
130
+ ...(version ? { version } : {}),
131
+ type: "library",
132
+ scope: "required",
133
+ properties,
134
+ };
135
+ if (purl) {
136
+ pkg.purl = purl;
137
+ // An environment can hold two builds of one version that differ only in
138
+ // their variants or compiler, and the DAG hash is what tells them apart,
139
+ // so it is carried in the bom-ref as a subpath to keep the refs distinct.
140
+ pkg["bom-ref"] = opts.hash
141
+ ? `${decodeURIComponent(purl)}#${opts.hash}`
142
+ : decodeURIComponent(purl);
143
+ } else {
144
+ pkg["bom-ref"] = `library:${name}:${version || ""}:${opts.hash || ""}`;
145
+ }
146
+ return pkg;
147
+ }
148
+
149
+ /**
150
+ * Render a node's architecture as `platform-os-target`.
151
+ *
152
+ * @param {object|undefined} arch Node `arch` object
153
+ * @returns {string|undefined} Architecture label
154
+ */
155
+ function architectureLabel(arch) {
156
+ if (!arch || typeof arch !== "object") {
157
+ return undefined;
158
+ }
159
+ // Newer lock versions nest the microarchitecture under `target.name`.
160
+ const target =
161
+ typeof arch.target === "string"
162
+ ? arch.target
163
+ : stringOrUndefined(arch.target?.name);
164
+ const parts = [
165
+ stringOrUndefined(arch.platform),
166
+ stringOrUndefined(arch.platform_os),
167
+ target,
168
+ ].filter(Boolean);
169
+ return parts.length ? parts.join("-") : undefined;
170
+ }
171
+
172
+ /**
173
+ * Render a node's compiler as `name@version`.
174
+ *
175
+ * @param {object|undefined} compiler Node `compiler` object
176
+ * @returns {string|undefined} Compiler label
177
+ */
178
+ function compilerLabel(compiler) {
179
+ const name = stringOrUndefined(compiler?.name);
180
+ if (!name) {
181
+ return undefined;
182
+ }
183
+ const version = stringOrUndefined(compiler?.version);
184
+ return version ? `${name}@${version}` : name;
185
+ }
186
+
187
+ /**
188
+ * Narrow a value to a non-empty string.
189
+ *
190
+ * @param {*} value Value to check
191
+ * @returns {string|undefined} The string, or undefined
192
+ */
193
+ function stringOrUndefined(value) {
194
+ return typeof value === "string" && value ? value : undefined;
195
+ }
196
+
197
+ /**
198
+ * Shared empty result for unreadable or malformed locks.
199
+ *
200
+ * @returns {{pkgList: object[], dependencies: object[], rootInputs: string[]}}
201
+ */
202
+ function emptyResult() {
203
+ return { pkgList: [], dependencies: [], rootInputs: [] };
204
+ }
@@ -0,0 +1,264 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { parse as loadYaml } from "yaml";
4
+
5
+ import { tryBuildPurl } from "../inventory/purl.js";
6
+
7
+ /**
8
+ * Haskell Stack parser for `stack.yaml` and its lock file.
9
+ *
10
+ * Stack resolves packages from a snapshot plus the `extra-deps` a project
11
+ * declares, and writes the completed locations to `stack.yaml.lock` (named
12
+ * after the configuration file, so a custom `foo.yaml` locks to
13
+ * `foo.yaml.lock`). The lock is YAML with two lists:
14
+ *
15
+ * - `packages` — one entry per extra-dep, each holding the `original`
16
+ * incomplete specification and the `completed` location. A Hackage
17
+ * location is the string `<name>-<version>@sha256:<hex>,<size>`; a
18
+ * repository location carries `url`, `commit` and `subdir`.
19
+ * - `snapshots` — the resolved snapshot chain, each with the `original`
20
+ * name (`lts-22.28`) and the `completed` url, size and sha256.
21
+ *
22
+ * The lock pins only the extra-deps: packages taken from the snapshot are not
23
+ * listed, because the snapshot url and digest already pin them. The snapshot
24
+ * is therefore recorded on the parent component rather than expanded.
25
+ *
26
+ * Hackage packages are identified with the registered `hackage` purl type
27
+ * (`pkg:hackage/<name>@<version>`).
28
+ */
29
+
30
+ /**
31
+ * Parse a Stack project from its lock file, with optional `stack.yaml`.
32
+ *
33
+ * @param {string} stackLockFile Path to `stack.yaml.lock` or `stack.lock`
34
+ * @param {string} [stackYamlFile] Path to `stack.yaml`, if present
35
+ * @returns {{ pkgList: object[], parentComponent: object, rootInputs: string[] }}
36
+ */
37
+ export function parseStackProject(stackLockFile, stackYamlFile) {
38
+ const lock = readYamlFile(stackLockFile);
39
+ const stackYaml = stackYamlFile ? readYamlFile(stackYamlFile) : undefined;
40
+ const snapshot = snapshotName(lock, stackYaml);
41
+
42
+ const pkgList = [];
43
+ const seen = new Set();
44
+ for (const entry of Array.isArray(lock?.packages) ? lock.packages : []) {
45
+ const pkg = stackPackageFromEntry(entry, stackLockFile);
46
+ if (!pkg || seen.has(pkg["bom-ref"])) {
47
+ continue;
48
+ }
49
+ seen.add(pkg["bom-ref"]);
50
+ pkgList.push(pkg);
51
+ }
52
+
53
+ const parentComponent = {};
54
+ if (stackYamlFile || pkgList.length) {
55
+ const name =
56
+ typeof stackYaml?.name === "string" && stackYaml.name
57
+ ? stackYaml.name
58
+ : projectDirName(stackYamlFile || stackLockFile);
59
+ parentComponent.type = "application";
60
+ parentComponent.name = name;
61
+ parentComponent.description = `Haskell Stack project: ${name}`;
62
+ parentComponent.properties = [
63
+ { name: "internal:SrcFile", value: stackYamlFile || stackLockFile },
64
+ ];
65
+ if (snapshot) {
66
+ parentComponent.properties.push({
67
+ name: "cdx:stack:snapshot",
68
+ value: snapshot,
69
+ });
70
+ }
71
+ }
72
+ return {
73
+ pkgList,
74
+ parentComponent,
75
+ rootInputs: pkgList.map((pkg) => pkg["bom-ref"]),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Build a package record from one `packages` entry of the lock.
81
+ *
82
+ * @param {object} entry Lock entry with `original` and `completed`
83
+ * @param {string} srcFile Lock file path for provenance
84
+ * @returns {object|undefined} Package record, or undefined when unrecognised
85
+ */
86
+ function stackPackageFromEntry(entry, srcFile) {
87
+ const completed = entry?.completed ?? {};
88
+ const original = entry?.original ?? {};
89
+ const hackage =
90
+ typeof completed.hackage === "string"
91
+ ? completed.hackage
92
+ : typeof original.hackage === "string"
93
+ ? original.hackage
94
+ : undefined;
95
+ if (hackage) {
96
+ return hackagePackage(hackage, srcFile);
97
+ }
98
+ // Repository extra-deps pin a commit rather than a Hackage release; the
99
+ // package name is the repository's last path segment unless a subdir names
100
+ // the package more precisely.
101
+ const url = typeof completed.url === "string" ? completed.url : undefined;
102
+ const commit =
103
+ typeof completed.commit === "string" ? completed.commit : undefined;
104
+ if (!url) {
105
+ return undefined;
106
+ }
107
+ const subdir =
108
+ typeof completed.subdir === "string" && completed.subdir
109
+ ? completed.subdir.split("/").filter(Boolean).pop()
110
+ : undefined;
111
+ const repoName = url
112
+ .split(/[?#]/u)[0]
113
+ .replace(/\.git$/u, "")
114
+ .split("/")
115
+ .filter(Boolean)
116
+ .pop();
117
+ const name = subdir || repoName;
118
+ if (!name) {
119
+ return undefined;
120
+ }
121
+ return finishPackage(name, commit, srcFile, [
122
+ { name: "cdx:stack:repository", value: url },
123
+ ...(commit ? [{ name: "cdx:stack:commit", value: commit }] : []),
124
+ ]);
125
+ }
126
+
127
+ /**
128
+ * Build a package record from a Hackage location string.
129
+ *
130
+ * The string is `<name>-<version>@sha256:<hex>,<size>`; the trailing
131
+ * `-<version>` separates the name, and the digest covers the package tarball.
132
+ *
133
+ * @param {string} location Hackage location as the lock writes it
134
+ * @param {string} srcFile Lock file path for provenance
135
+ * @returns {object|undefined} Package record
136
+ */
137
+ function hackagePackage(location, srcFile) {
138
+ const [coordinate, rest] = splitOnce(location, "@");
139
+ const match = coordinate.match(/^(.+)-(\d[^-]*)$/u);
140
+ if (!match) {
141
+ return undefined;
142
+ }
143
+ const [, name, version] = match;
144
+ const sha256 = rest?.startsWith("sha256:")
145
+ ? rest.slice("sha256:".length).split(",")[0]
146
+ : undefined;
147
+ const pkg = finishPackage(name, version, srcFile, []);
148
+ if (sha256 && /^[0-9a-f]{64}$/iu.test(sha256)) {
149
+ pkg.hashes = [{ alg: "SHA-256", content: sha256.toLowerCase() }];
150
+ }
151
+ return pkg;
152
+ }
153
+
154
+ /**
155
+ * Assemble the common fields of a Stack package record.
156
+ *
157
+ * @param {string} name Package name
158
+ * @param {string|undefined} version Version or pinned commit
159
+ * @param {string} srcFile Lock file path for provenance
160
+ * @param {object[]} extraProperties Additional properties
161
+ * @returns {object} Package record
162
+ */
163
+ function finishPackage(name, version, srcFile, extraProperties) {
164
+ const purl = tryBuildPurl({
165
+ type: "hackage",
166
+ name,
167
+ version: version || undefined,
168
+ });
169
+ const pkg = {
170
+ name,
171
+ ...(version ? { version } : {}),
172
+ type: "library",
173
+ scope: "required",
174
+ properties: [
175
+ { name: "internal:SrcFile", value: srcFile },
176
+ // The lock records extra-deps only; snapshot packages never appear.
177
+ { name: "cdx:stack:dependency", value: "extra-dep" },
178
+ ...extraProperties,
179
+ ],
180
+ };
181
+ if (purl) {
182
+ pkg.purl = purl;
183
+ pkg["bom-ref"] = decodeURIComponent(purl);
184
+ } else {
185
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
186
+ }
187
+ return pkg;
188
+ }
189
+
190
+ /**
191
+ * Determine the snapshot the project resolves against.
192
+ *
193
+ * `stack.yaml` names it under `snapshot` (or the older `resolver`); the lock
194
+ * repeats it as the `original` of the first snapshot entry.
195
+ *
196
+ * @param {object|undefined} lock Parsed lock file
197
+ * @param {object|undefined} stackYaml Parsed `stack.yaml`
198
+ * @returns {string|undefined} Snapshot name or url
199
+ */
200
+ function snapshotName(lock, stackYaml) {
201
+ for (const candidate of [stackYaml?.snapshot, stackYaml?.resolver]) {
202
+ if (typeof candidate === "string" && candidate) {
203
+ return candidate;
204
+ }
205
+ }
206
+ const first = Array.isArray(lock?.snapshots) ? lock.snapshots[0] : undefined;
207
+ const original = first?.original;
208
+ if (typeof original === "string" && original) {
209
+ return original;
210
+ }
211
+ if (typeof original?.url === "string" && original.url) {
212
+ return original.url;
213
+ }
214
+ if (typeof first?.completed?.url === "string") {
215
+ return first.completed.url;
216
+ }
217
+ return undefined;
218
+ }
219
+
220
+ /**
221
+ * Split a string at the first occurrence of a separator.
222
+ *
223
+ * @param {string} value Value to split
224
+ * @param {string} separator Separator character
225
+ * @returns {[string, string|undefined]} Head and remainder
226
+ */
227
+ function splitOnce(value, separator) {
228
+ const index = value.indexOf(separator);
229
+ if (index === -1) {
230
+ return [value, undefined];
231
+ }
232
+ return [value.slice(0, index), value.slice(index + 1)];
233
+ }
234
+
235
+ /**
236
+ * Derive a project name from the directory holding the Stack files.
237
+ *
238
+ * Stack manifests never carry a project name, so the directory is the name
239
+ * developers already use for the project.
240
+ *
241
+ * @param {string} filePath File path
242
+ * @returns {string} Directory name
243
+ */
244
+ function projectDirName(filePath) {
245
+ const segments = filePath.split(/[\\/]/u).filter(Boolean);
246
+ // Drop the file itself, keeping the directory.
247
+ segments.pop();
248
+ return segments[segments.length - 1] || "stack-project";
249
+ }
250
+
251
+ /**
252
+ * Read and parse a YAML file, warning instead of throwing.
253
+ *
254
+ * @param {string} filePath File to read
255
+ * @returns {object|undefined} Parsed value
256
+ */
257
+ function readYamlFile(filePath) {
258
+ try {
259
+ return loadYaml(readFileSync(filePath, "utf-8"));
260
+ } catch (error) {
261
+ console.warn(`Failed to parse ${filePath}: ${error.message}`);
262
+ return undefined;
263
+ }
264
+ }
@@ -0,0 +1,194 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { tryBuildPurl } from "../inventory/purl.js";
4
+
5
+ /**
6
+ * Terraform and OpenTofu parser for `.terraform.lock.hcl`.
7
+ *
8
+ * The lock file pins every provider the configuration resolves to, with its
9
+ * exact version, the version constraints that produced it, and content
10
+ * hashes. It is written in a small, regular subset of HCL: provider blocks,
11
+ * `version` and `constraints` attributes, and a `hashes` list of prefixed
12
+ * digests. A line scanner is sufficient and avoids a general HCL parser.
13
+ *
14
+ * Provider addresses look like `registry.terraform.io/hashicorp/aws`; the
15
+ * registry host and namespace identify the provider upstream, so they are
16
+ * kept in the component group and a generic purl
17
+ * (`pkg:generic/<host>/<namespace>/<type>@<version>`), with the intended type
18
+ * recorded as a `cdx:purl:proposedType` property because no `terraform`
19
+ * purl type is registered.
20
+ *
21
+ * The two hash schemes in the `hashes` list mean different things. A `zh:`
22
+ * digest is a SHA-256 of the official `.zip` package as the registry indexes
23
+ * it, so it is the one a consumer can check an artifact against and the one
24
+ * emitted in the CycloneDX `hashes` array. An `h1:` digest is base64 over a
25
+ * hash of the package's *contents* rather than the archive, which lets
26
+ * Terraform verify an unpacked directory but makes it useless as an artifact
27
+ * checksum; it is kept as a property instead of being mislabelled SHA-256.
28
+ */
29
+
30
+ /**
31
+ * Parse a `.terraform.lock.hcl` file.
32
+ *
33
+ * @param {string} lockFile Path to the lock file
34
+ * @returns {{ pkgList: object[] }} Provider components
35
+ */
36
+ export function parseTerraformLockFile(lockFile) {
37
+ let text;
38
+ try {
39
+ text = readFileSync(lockFile, "utf-8");
40
+ } catch (error) {
41
+ console.warn(`Failed to read ${lockFile}: ${error.message}`);
42
+ return { pkgList: [] };
43
+ }
44
+
45
+ const pkgList = [];
46
+ let current;
47
+ let inHashes = false;
48
+ for (const rawLine of text.split("\n")) {
49
+ const line = rawLine.trim();
50
+ if (!line || line.startsWith("#")) {
51
+ continue;
52
+ }
53
+ if (inHashes) {
54
+ if (line.startsWith("]")) {
55
+ inHashes = false;
56
+ continue;
57
+ }
58
+ const hash = line.match(/^"([^"]+)"/u);
59
+ if (hash && current) {
60
+ current.candidates.push(hash[1]);
61
+ }
62
+ continue;
63
+ }
64
+ const block = line.match(/^provider\s+"([^"]+)"\s*\{/u);
65
+ if (block) {
66
+ current = { address: block[1], candidates: [] };
67
+ continue;
68
+ }
69
+ if (line.startsWith("}")) {
70
+ if (current) {
71
+ const pkg = buildProviderComponent(current, lockFile);
72
+ if (pkg) {
73
+ pkgList.push(pkg);
74
+ }
75
+ current = undefined;
76
+ }
77
+ continue;
78
+ }
79
+ const version = matchAttribute(line, "version");
80
+ if (version && current) {
81
+ current.version = version;
82
+ continue;
83
+ }
84
+ const constraints = matchAttribute(line, "constraints");
85
+ if (constraints && current) {
86
+ current.constraints = constraints;
87
+ continue;
88
+ }
89
+ if (/^hashes\s*=\s*\[/u.test(line) && current) {
90
+ inHashes = true;
91
+ // Some writers put the first digest on the same line as the bracket.
92
+ const inline = line.replace(/^hashes\s*=\s*\[\s*/u, "");
93
+ const inlineHash = inline.match(/^"([^"]+)"/u);
94
+ if (inlineHash) {
95
+ current.candidates.push(inlineHash[1]);
96
+ }
97
+ }
98
+ }
99
+ return { pkgList };
100
+ }
101
+
102
+ /**
103
+ * Match a string attribute assignment such as `version = "5.80.0"`.
104
+ *
105
+ * @param {string} line Trimmed line
106
+ * @param {string} name Attribute name
107
+ * @returns {string|undefined} Attribute value when present
108
+ */
109
+ function matchAttribute(line, name) {
110
+ const match = line.match(new RegExp(`^${name}\\s*=\\s*"([^"]*)"`, "u"));
111
+ return match ? match[1] : undefined;
112
+ }
113
+
114
+ /**
115
+ * Build a provider component from one parsed block.
116
+ *
117
+ * @param {{address: string, version?: string, constraints?: string, candidates: string[]}} block
118
+ * Parsed provider block
119
+ * @param {string} srcFile Lock file path
120
+ * @returns {object|undefined} Component record
121
+ */
122
+ function buildProviderComponent(block, srcFile) {
123
+ const segments = block.address.split("/").filter(Boolean);
124
+ if (segments.length < 2) {
125
+ return undefined;
126
+ }
127
+ const name = segments[segments.length - 1];
128
+ const namespace = segments.slice(0, -1).join("/");
129
+ const purl = tryBuildPurl({
130
+ type: "generic",
131
+ namespace,
132
+ name,
133
+ version: block.version,
134
+ });
135
+ const properties = [
136
+ { name: "internal:SrcFile", value: srcFile },
137
+ { name: "cdx:purl:proposedType", value: "terraform-provider" },
138
+ { name: "cdx:tf:address", value: block.address },
139
+ ];
140
+ if (block.constraints) {
141
+ properties.push({ name: "cdx:tf:constraints", value: block.constraints });
142
+ }
143
+ const contentHash = block.candidates.find((candidate) =>
144
+ candidate.startsWith("h1:"),
145
+ );
146
+ if (contentHash) {
147
+ properties.push({ name: "cdx:tf:h1", value: contentHash });
148
+ }
149
+ const pkg = {
150
+ group: namespace,
151
+ name,
152
+ ...(block.version ? { version: block.version } : {}),
153
+ type: "library",
154
+ scope: "required",
155
+ properties,
156
+ };
157
+ const sha256 = zipDigestsHex(block.candidates);
158
+ if (sha256.length) {
159
+ pkg.hashes = sha256.map((content) => ({ alg: "SHA-256", content }));
160
+ }
161
+ if (purl) {
162
+ pkg.purl = purl;
163
+ pkg["bom-ref"] = decodeURIComponent(purl);
164
+ } else {
165
+ pkg["bom-ref"] = `library:${block.address}:${block.version || ""}`;
166
+ }
167
+ return pkg;
168
+ }
169
+
170
+ /**
171
+ * Collect the `zh:` package digests of a provider block.
172
+ *
173
+ * A provider is published as one zip per platform and the lock records a
174
+ * digest for each, so a block yields several digests for the same version.
175
+ * All of them are returned: each is a genuine SHA-256 of a distributed
176
+ * artifact for this component, and the lock does not say which platform a
177
+ * given digest belongs to.
178
+ *
179
+ * @param {string[]} candidates Prefixed digests from the hashes list
180
+ * @returns {string[]} Lowercase hex digests, in file order and deduplicated
181
+ */
182
+ function zipDigestsHex(candidates) {
183
+ const digests = new Set();
184
+ for (const candidate of candidates) {
185
+ if (!candidate.startsWith("zh:")) {
186
+ continue;
187
+ }
188
+ const digest = candidate.slice(3).toLowerCase();
189
+ if (/^[0-9a-f]{64}$/u.test(digest)) {
190
+ digests.add(digest);
191
+ }
192
+ }
193
+ return [...digests];
194
+ }