@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
@@ -8,7 +8,6 @@ import {
8
8
  relative,
9
9
  resolve,
10
10
  } from "node:path";
11
- import process from "node:process";
12
11
 
13
12
  import { build, Purl } from "@cdxgen/cdx-purl";
14
13
  import { parse as _load } from "yaml";
@@ -18,6 +17,8 @@ import {
18
17
  DEBUG_MODE,
19
18
  readEnvironmentVariable,
20
19
  } from "../core/activity.js";
20
+ import { recordDegradation } from "../core/buildLedger.js";
21
+ import { deferFailOnError } from "../core/deferredExit.js";
21
22
  import { shouldFetchLicense } from "../core/env.js";
22
23
  import {
23
24
  safeExistsSync,
@@ -40,6 +41,7 @@ import {
40
41
  mapConanPkgRefToPurlStringAndNameAndVersion,
41
42
  nixGenericPurl,
42
43
  purlFromUrlString,
44
+ tryBuildPurl,
43
45
  } from "../inventory/purl.js";
44
46
  import { getLicenses, spdxLicenses } from "../inventory/spdx.js";
45
47
  import { getDartMetadata } from "./ecosystems.js";
@@ -2652,6 +2654,15 @@ export function executePodCommand(parameters, path, options) {
2652
2654
  );
2653
2655
  }
2654
2656
  if (readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true") {
2657
+ recordDegradation("cocoa.pods.missing", {
2658
+ ecosystem: "cocoa",
2659
+ tool: "pod",
2660
+ impact: "transitive-deps",
2661
+ command: `pod ${parameters.join(" ")}`,
2662
+ exitCode: typeof result.status === "number" ? result.status : undefined,
2663
+ detail:
2664
+ "The pod command did not succeed, so the cocoapods dependency graph was not resolved.",
2665
+ });
2655
2666
  console.log(
2656
2667
  "Consider using the cdxgen container image (`ghcr.io/cdxgen/cdxgen`), which includes cocoapods and additional build tools.",
2657
2668
  );
@@ -2667,7 +2678,11 @@ export function executePodCommand(parameters, path, options) {
2667
2678
  if (result.stderr) {
2668
2679
  console.log(result.stderr);
2669
2680
  }
2670
- options.failOnError && process.exit(1);
2681
+ deferFailOnError(options, {
2682
+ ecosystem: "cocoa",
2683
+ tool: "pod",
2684
+ detail: "the pod command failed",
2685
+ });
2671
2686
  }
2672
2687
  }
2673
2688
  return result;
@@ -3189,3 +3204,174 @@ async function fullScanCocoaPod(dependency, component, options) {
3189
3204
  component.externalReferences = externalRefs;
3190
3205
  }
3191
3206
  }
3207
+
3208
+ /**
3209
+ * Parse an xmake `xmake-requires.lock` file.
3210
+ *
3211
+ * The lock is a Lua table, written when the `package.requires_lock` policy is
3212
+ * enabled. Its top level holds a `__meta__` entry plus one section per build
3213
+ * configuration, keyed `platform|arch`; each section maps a requirement key
3214
+ * (`"zlib#31fecfc4"`, or `"sol2 v3.2.1#72267bd5"` when the requirement
3215
+ * carries a version) to a table with the resolved `version` and the
3216
+ * `repo` revision the package definition came from.
3217
+ *
3218
+ * The same package appears once per configuration, so entries are collapsed
3219
+ * by name and version, and the configurations a package was resolved for are
3220
+ * kept as a property. A requirement key without a `version` field names a
3221
+ * system package xmake did not resolve; it is inventoried without a version.
3222
+ *
3223
+ * No `xmake` purl type is registered, so packages are identified as generic
3224
+ * carrying a `cdx:purl:proposedType=xmake` property, following the
3225
+ * convention used for nix and zig.
3226
+ *
3227
+ * @param {string} lockFile Path to `xmake-requires.lock`
3228
+ * @returns {Object[]} Package records for the pinned requirements
3229
+ */
3230
+ export function parseXmakeRequiresLock(lockFile) {
3231
+ let text;
3232
+ try {
3233
+ text = readFileSync(lockFile, "utf-8");
3234
+ } catch (error) {
3235
+ console.warn(`Failed to read ${lockFile}: ${error.message}`);
3236
+ return [];
3237
+ }
3238
+ const byKey = new Map();
3239
+ // The outermost `{` is the file's own table, so a configuration section
3240
+ // opens at depth 1 and a requirement inside it at depth 2. Each is dropped
3241
+ // again once the brace depth falls back to where it opened.
3242
+ const CONFIGURATION_DEPTH = 1;
3243
+ const REQUIREMENT_DEPTH = 2;
3244
+ let configuration;
3245
+ let requirement;
3246
+ let depth = 0;
3247
+ for (const rawLine of text.split("\n")) {
3248
+ const line = rawLine.trim();
3249
+ if (!line || line.startsWith("--")) {
3250
+ continue;
3251
+ }
3252
+ // `["macosx|arm64"] = {` opens a configuration; `["zlib#hash"] = {` opens
3253
+ // a requirement inside one.
3254
+ const opener = line.match(/^\[\s*"([^"]+)"\s*\]\s*=\s*\{/u);
3255
+ if (opener && depth === CONFIGURATION_DEPTH) {
3256
+ // `__meta__` is normally written unquoted, but a lock that quotes it
3257
+ // must not become a configuration section either.
3258
+ configuration = opener[1] === "__meta__" ? undefined : opener[1];
3259
+ } else if (opener && depth === REQUIREMENT_DEPTH && configuration) {
3260
+ requirement = xmakeRequirementName(opener[1]);
3261
+ const key = `${configuration}|${requirement}`;
3262
+ if (requirement && !byKey.has(key)) {
3263
+ byKey.set(key, { name: requirement, configuration });
3264
+ }
3265
+ } else if (!opener && configuration && requirement) {
3266
+ const version = line.match(/^version\s*=\s*"([^"]*)"/u);
3267
+ if (version) {
3268
+ const record = byKey.get(`${configuration}|${requirement}`);
3269
+ if (record) {
3270
+ record.version = version[1];
3271
+ }
3272
+ }
3273
+ }
3274
+ depth = Math.max(0, depth + countXmakeBraces(line));
3275
+ if (depth <= REQUIREMENT_DEPTH) {
3276
+ requirement = undefined;
3277
+ }
3278
+ if (depth <= CONFIGURATION_DEPTH) {
3279
+ configuration = undefined;
3280
+ }
3281
+ }
3282
+
3283
+ // Collapse the per-configuration entries into one component per resolved
3284
+ // coordinate, listing the configurations it was resolved for.
3285
+ const merged = new Map();
3286
+ for (const record of byKey.values()) {
3287
+ const key = `${record.name}@${record.version || ""}`;
3288
+ const existing = merged.get(key);
3289
+ if (existing) {
3290
+ existing.configurations.add(record.configuration);
3291
+ continue;
3292
+ }
3293
+ merged.set(key, {
3294
+ name: record.name,
3295
+ version: record.version,
3296
+ configurations: new Set([record.configuration]),
3297
+ });
3298
+ }
3299
+
3300
+ const pkgList = [];
3301
+ for (const record of merged.values()) {
3302
+ // A malformed coordinate in an untrusted lock costs one entry, not the
3303
+ // whole scan.
3304
+ const purl = tryBuildPurl({
3305
+ type: "generic",
3306
+ namespace: "xmake",
3307
+ name: record.name,
3308
+ version: record.version || null,
3309
+ });
3310
+ if (!purl) {
3311
+ continue;
3312
+ }
3313
+ pkgList.push({
3314
+ name: record.name,
3315
+ ...(record.version ? { version: record.version } : {}),
3316
+ type: "library",
3317
+ scope: "required",
3318
+ purl,
3319
+ "bom-ref": decodeURIComponent(purl),
3320
+ properties: [
3321
+ { name: "internal:SrcFile", value: lockFile },
3322
+ { name: "cdx:purl:proposedType", value: "xmake" },
3323
+ {
3324
+ name: "cdx:xmake:configurations",
3325
+ value: [...record.configurations].sort().join(", "),
3326
+ },
3327
+ ],
3328
+ evidence: {
3329
+ identity: {
3330
+ field: "purl",
3331
+ confidence: 0.5,
3332
+ methods: [
3333
+ {
3334
+ technique: "source-code-analysis",
3335
+ confidence: 0.5,
3336
+ value: `Filename ${lockFile}`,
3337
+ },
3338
+ ],
3339
+ },
3340
+ },
3341
+ });
3342
+ }
3343
+ return pkgList;
3344
+ }
3345
+
3346
+ /**
3347
+ * Reduce an xmake requirement key to the package name.
3348
+ *
3349
+ * Keys look like `zlib#31fecfc4` or `sol2 v3.2.1#72267bd5`: the trailing
3350
+ * `#<hash>` identifies the resolved configuration and the optional second
3351
+ * token is the requested version range, neither of which is the name.
3352
+ *
3353
+ * @param {string} key Requirement key as written in the lock
3354
+ * @returns {string|undefined} Package name
3355
+ */
3356
+ function xmakeRequirementName(key) {
3357
+ const name = key.split("#")[0].trim().split(/\s+/u)[0];
3358
+ return name || undefined;
3359
+ }
3360
+
3361
+ /**
3362
+ * Net brace change contributed by one line of a Lua table.
3363
+ *
3364
+ * @param {string} line Line to scan
3365
+ * @returns {number} Opens minus closes
3366
+ */
3367
+ function countXmakeBraces(line) {
3368
+ let delta = 0;
3369
+ for (const char of line) {
3370
+ if (char === "{") {
3371
+ delta += 1;
3372
+ } else if (char === "}") {
3373
+ delta -= 1;
3374
+ }
3375
+ }
3376
+ return delta;
3377
+ }
@@ -0,0 +1,221 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+
4
+ import { tryBuildPurl } from "../inventory/purl.js";
5
+
6
+ /**
7
+ * Nim parser for `.nimble` manifests and `nimble.lock`.
8
+ *
9
+ * A `.nimble` file is a Nim script whose practical surface is a set of
10
+ * `key = "value"` assignments plus `requires "dep >= 1.0"` lines; it is read
11
+ * with a line scanner over those shapes rather than a Nim interpreter.
12
+ * Nimble persists the resolved dependency set to `nimble.lock` (JSON), which
13
+ * is the source of truth for versions.
14
+ *
15
+ * No `nim` purl type is registered, so packages are identified as generic
16
+ * carrying a `cdx:purl:proposedType=nim` property, following the convention
17
+ * used for nix and zig.
18
+ */
19
+
20
+ /**
21
+ * Parse a Nim project from a `.nimble` manifest and optional `nimble.lock`.
22
+ *
23
+ * @param {string} nimbleFile Path to the `.nimble` file
24
+ * @param {string} [nimbleLockFile] Path to `nimble.lock`, if present
25
+ * @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs?: string[] }}
26
+ */
27
+ export function parseNimProject(nimbleFile, nimbleLockFile) {
28
+ const fields = readNimbleFile(nimbleFile);
29
+ const parentComponent = {};
30
+ // Nimble files frequently omit `name`; the manifest file name is the
31
+ // package name in that case, which is also how nimble itself refers to it.
32
+ const projectName =
33
+ fields.name || basename(nimbleFile).replace(/\.nimble$/iu, "");
34
+ if (projectName) {
35
+ parentComponent.type = "application";
36
+ parentComponent.name = projectName;
37
+ if (fields.version) {
38
+ parentComponent.version = fields.version;
39
+ }
40
+ parentComponent.description =
41
+ fields.description || `Nim package: ${fields.name}`;
42
+ parentComponent.properties = [
43
+ { name: "internal:SrcFile", value: nimbleFile },
44
+ ];
45
+ if (fields.license) {
46
+ parentComponent.license = fields.license;
47
+ }
48
+ }
49
+
50
+ let lock;
51
+ if (nimbleLockFile) {
52
+ try {
53
+ lock = JSON.parse(readFileSync(nimbleLockFile, "utf-8"));
54
+ } catch (error) {
55
+ console.warn(`Failed to parse ${nimbleLockFile}: ${error.message}`);
56
+ lock = undefined;
57
+ }
58
+ }
59
+
60
+ // nimble.lock has carried `packages` both as an object keyed by name and as
61
+ // an array of records; both shapes are accepted so locks from either
62
+ // nimble generation parse.
63
+ const lockedEntries = [];
64
+ if (Array.isArray(lock?.packages)) {
65
+ lockedEntries.push(...lock.packages);
66
+ } else if (lock?.packages && typeof lock.packages === "object") {
67
+ for (const [name, entry] of Object.entries(lock.packages)) {
68
+ lockedEntries.push({ name, ...entry });
69
+ }
70
+ }
71
+
72
+ if (!lockedEntries.length) {
73
+ // Lockless fallback: emit declared requirements without resolved versions.
74
+ const pkgList = fields.requires.map((req) =>
75
+ nimPackage(req.name, undefined, { direct: true, srcFile: nimbleFile }),
76
+ );
77
+ return {
78
+ pkgList,
79
+ dependencies: [],
80
+ parentComponent,
81
+ rootInputs: pkgList.map((p) => p["bom-ref"]),
82
+ };
83
+ }
84
+
85
+ const directNames = new Set(fields.requires.map((req) => req.name));
86
+ const pkgList = [];
87
+ const refByName = new Map();
88
+ const edges = [];
89
+ for (const entry of lockedEntries) {
90
+ if (typeof entry?.name !== "string" || !entry.name) {
91
+ continue;
92
+ }
93
+ const version =
94
+ typeof entry.version === "string" && entry.version
95
+ ? entry.version
96
+ : undefined;
97
+ const pkg = nimPackage(entry.name, version, {
98
+ direct: directNames.has(entry.name),
99
+ vcsRevision:
100
+ typeof entry.vcsRevision === "string" && entry.vcsRevision
101
+ ? entry.vcsRevision
102
+ : undefined,
103
+ srcFile: nimbleLockFile,
104
+ });
105
+ pkgList.push(pkg);
106
+ refByName.set(entry.name, pkg["bom-ref"]);
107
+ }
108
+ for (const entry of lockedEntries) {
109
+ const ref = refByName.get(entry.name);
110
+ if (!ref) {
111
+ continue;
112
+ }
113
+ const dependsOn = (
114
+ Array.isArray(entry.dependencies) ? entry.dependencies : []
115
+ )
116
+ .map((depName) =>
117
+ typeof depName === "string" ? refByName.get(depName) : undefined,
118
+ )
119
+ .filter(Boolean);
120
+ edges.push({ ref, dependsOn: [...new Set(dependsOn)].sort() });
121
+ }
122
+ const rootInputs = [...directNames]
123
+ .map((name) => refByName.get(name))
124
+ .filter(Boolean);
125
+ return { pkgList, dependencies: edges, parentComponent, rootInputs };
126
+ }
127
+
128
+ /**
129
+ * Build a component-like package record for a Nim package.
130
+ *
131
+ * @param {string} name Package name
132
+ * @param {string|undefined} version Resolved version, if known
133
+ * @param {object} opts Extra context (`direct`, `vcsRevision`, `srcFile`)
134
+ * @returns {object} Package record
135
+ */
136
+ function nimPackage(name, version, opts) {
137
+ const purl = tryBuildPurl({
138
+ type: "generic",
139
+ name,
140
+ version: version || undefined,
141
+ });
142
+ const properties = [
143
+ { name: "internal:SrcFile", value: opts.srcFile },
144
+ { name: "cdx:purl:proposedType", value: "nim" },
145
+ {
146
+ name: "cdx:nim:dependency",
147
+ value: opts.direct ? "direct" : "transitive",
148
+ },
149
+ ];
150
+ if (opts.vcsRevision) {
151
+ properties.push({ name: "cdx:nim:vcsRevision", value: opts.vcsRevision });
152
+ }
153
+ const pkg = {
154
+ name,
155
+ ...(version ? { version } : {}),
156
+ type: "library",
157
+ scope: "required",
158
+ properties,
159
+ };
160
+ if (purl) {
161
+ pkg.purl = purl;
162
+ pkg["bom-ref"] = decodeURIComponent(purl);
163
+ } else {
164
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
165
+ }
166
+ return pkg;
167
+ }
168
+
169
+ /**
170
+ * Read the assignments and requirements of a `.nimble` file.
171
+ *
172
+ * @param {string} filePath File to read
173
+ * @returns {{name?: string, version?: string, description?: string, license?: string, requires: Array<{name: string, constraint?: string}>}}
174
+ */
175
+ export function readNimbleFile(filePath) {
176
+ const result = { requires: [] };
177
+ let text;
178
+ try {
179
+ text = readFileSync(filePath, "utf-8");
180
+ } catch (error) {
181
+ console.warn(`Failed to read ${filePath}: ${error.message}`);
182
+ return result;
183
+ }
184
+ for (const rawLine of text.split("\n")) {
185
+ const line = rawLine.trim();
186
+ if (!line || line.startsWith("#")) {
187
+ continue;
188
+ }
189
+ const assignment = line.match(
190
+ /^(name|version|description|license)\s*=\s*"([^"]*)"/u,
191
+ );
192
+ if (assignment) {
193
+ const value = assignment[2];
194
+ if (value) {
195
+ result[assignment[1]] = value;
196
+ }
197
+ continue;
198
+ }
199
+ const requires = line.match(/^requires\s+"([^"]+)"/u);
200
+ if (requires) {
201
+ const tokens = requires[1].split(/\s+/u);
202
+ const first = tokens[0];
203
+ const constraint = tokens.slice(1).join(" ");
204
+ if (first.startsWith("http") || first.includes("://")) {
205
+ // URL requirements are identified by the repository name, which is
206
+ // how nimble itself keys them in the lock file. A trailing .nim
207
+ // extension is dropped because the lock keys by the bare name.
208
+ const withoutExtension = first.split(/[?#]/u)[0].replace(/\.git$/u, "");
209
+ const segments = withoutExtension.split("/").filter(Boolean);
210
+ const repoName = segments[segments.length - 1] || first;
211
+ result.requires.push({
212
+ name: repoName.replace(/\.nim$/iu, ""),
213
+ constraint,
214
+ });
215
+ continue;
216
+ }
217
+ result.requires.push({ name: first, constraint });
218
+ }
219
+ }
220
+ return result;
221
+ }
@@ -0,0 +1,180 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { tryBuildPurl } from "../inventory/purl.js";
4
+
5
+ /**
6
+ * OCaml opam parser for `.opam.locked` files.
7
+ *
8
+ * `opam lock` writes a `<name>.opam.locked` file that pins the full resolved
9
+ * dependency set of a project: a `depends:` field listing every package with
10
+ * an equality constraint on its version, and a `pin-depends:` field for
11
+ * packages pinned to a URL. The file is opam's own syntax, a small
12
+ * line-oriented field language, which is read here with a line scanner rather
13
+ * than a monolithic expression.
14
+ *
15
+ * A locked file holds the closure only: it does not record which packages are
16
+ * direct dependencies of the project, so every entry is reported with the
17
+ * same standing and the project root links to all of them.
18
+ *
19
+ * Packages are identified with the registered `opam` purl type
20
+ * (`pkg:opam/<name>@<version>`).
21
+ */
22
+
23
+ /**
24
+ * Parse an `.opam.locked` (or plain `.opam`) file.
25
+ *
26
+ * @param {string} opamLockedFile Path to the opam file
27
+ * @returns {{ pkgList: object[], pins: Array<{name: string, url: string}> }}
28
+ */
29
+ export function parseOpamLockedFile(opamLockedFile) {
30
+ let text;
31
+ try {
32
+ text = readFileSync(opamLockedFile, "utf-8");
33
+ } catch (error) {
34
+ console.warn(`Failed to read ${opamLockedFile}: ${error.message}`);
35
+ return { pkgList: [], pins: [] };
36
+ }
37
+
38
+ const depends = [];
39
+ const pins = [];
40
+ const lines = text.split("\n");
41
+ let section;
42
+ for (const rawLine of lines) {
43
+ const line = rawLine.trim();
44
+ if (!line || line.startsWith("#")) {
45
+ continue;
46
+ }
47
+ if (line.startsWith("depends:")) {
48
+ section = "depends";
49
+ continue;
50
+ }
51
+ if (line.startsWith("pin-depends:")) {
52
+ section = "pin-depends";
53
+ continue;
54
+ }
55
+ // A bare `field:` at the start of a line ends the previous list section.
56
+ if (/^[A-Za-z0-9_-]+:/u.test(line)) {
57
+ section = undefined;
58
+ continue;
59
+ }
60
+ if (section === "depends") {
61
+ collectDependsEntry(line, depends, opamLockedFile);
62
+ } else if (section === "pin-depends") {
63
+ collectPinEntry(line, pins);
64
+ }
65
+ }
66
+ // A pin overrides where a package is fetched from; reflect that on the
67
+ // resolved component so consumers can see the divergence from the registry.
68
+ for (const pin of pins) {
69
+ const target = depends.find((dep) => dep.name === pin.name);
70
+ if (target) {
71
+ target.properties.push({ name: "cdx:opam:pinned", value: "true" });
72
+ target.properties.push({
73
+ name: "cdx:opam:pinSource",
74
+ value: pin.url,
75
+ });
76
+ }
77
+ }
78
+ return { pkgList: depends, pins };
79
+ }
80
+
81
+ /**
82
+ * Turn one `depends` list line into a pinned package record.
83
+ *
84
+ * Entries look like `"dune" {= "3.16.0"}` or `"ocaml" {= "5.1.1" & build}`.
85
+ * Only equality pins carry a version; constraint-only entries are recorded
86
+ * without one so the package is still inventoried.
87
+ *
88
+ * @param {string} line Trimmed list line
89
+ * @param {object[]} depends Accumulator for package records
90
+ * @param {string} srcFile Source path for provenance
91
+ */
92
+ function collectDependsEntry(line, depends, srcFile) {
93
+ const nameMatch = line.match(/^"([^"]+)"/u);
94
+ if (!nameMatch) {
95
+ return;
96
+ }
97
+ const name = nameMatch[1];
98
+ let version;
99
+ const constraints = line.match(/\{[^}]*\}/gu) || [];
100
+ for (const constraint of constraints) {
101
+ // A pin is a standalone `=`, not the tail of `>=`, `<=`, or `!=`.
102
+ const pinned = constraint.match(/(?<![<>!=])=(?!=)\s*"([^"]+)"/u);
103
+ if (pinned) {
104
+ version = pinned[1];
105
+ break;
106
+ }
107
+ }
108
+ if (depends.some((dep) => dep.name === name && dep.version === version)) {
109
+ return;
110
+ }
111
+ const purl = tryBuildPurl({
112
+ type: "opam",
113
+ name,
114
+ version: version || undefined,
115
+ });
116
+ const properties = [{ name: "internal:SrcFile", value: srcFile }];
117
+ const pkg = {
118
+ name,
119
+ ...(version ? { version } : {}),
120
+ type: "library",
121
+ scope: "required",
122
+ properties,
123
+ };
124
+ if (purl) {
125
+ pkg.purl = purl;
126
+ pkg["bom-ref"] = decodeURIComponent(purl);
127
+ } else {
128
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
129
+ }
130
+ depends.push(pkg);
131
+ }
132
+
133
+ /**
134
+ * Turn one `pin-depends` list line into a pin record.
135
+ *
136
+ * Entries look like `[ "mylib.dev" "git+https://host/path#rev" ]`. The first
137
+ * string is a package *version* identifier — `<name>.<version>` — whereas the
138
+ * matching `depends` entry carries the bare name, so the version suffix is
139
+ * split off before the two can be matched. Package names cannot contain a
140
+ * dot, which makes the first dot the separator.
141
+ *
142
+ * The URL is reduced to its origin and path so credentials embedded in a
143
+ * query string never reach the BOM.
144
+ *
145
+ * @param {string} line Trimmed list line
146
+ * @param {Array<{name: string, url: string}>} pins Accumulator for pins
147
+ */
148
+ function collectPinEntry(line, pins) {
149
+ const strings = line.match(/"[^"]*"/gu) || [];
150
+ if (strings.length < 2) {
151
+ return;
152
+ }
153
+ const identifier = strings[0].slice(1, -1);
154
+ const rawUrl = strings[1].slice(1, -1);
155
+ pins.push({
156
+ name: identifier.split(".")[0],
157
+ url: sanitizedPinUrl(rawUrl),
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Reduce a pin URL to a safe origin plus path.
163
+ *
164
+ * @param {string} rawUrl URL as written in the lock file
165
+ * @returns {string} Sanitised URL, or the raw value when it is not a URL
166
+ */
167
+ function sanitizedPinUrl(rawUrl) {
168
+ const schemeSplit = rawUrl.split("://");
169
+ if (schemeSplit.length < 2) {
170
+ return rawUrl;
171
+ }
172
+ const scheme = schemeSplit[0];
173
+ const rest = schemeSplit[1];
174
+ const pathStart = rest.indexOf("/");
175
+ const authority = pathStart === -1 ? rest : rest.slice(0, pathStart);
176
+ const path = pathStart === -1 ? "" : rest.slice(pathStart).split(/[?#]/u)[0];
177
+ // Userinfo is dropped by taking only the authority's host portion.
178
+ const host = authority.split("@").pop();
179
+ return `${scheme}://${host}${path}`;
180
+ }