@cdxgen/cdxgen 13.1.0 → 13.3.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 (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -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/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -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
+ }
@@ -0,0 +1,277 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { tryBuildPurl } from "../inventory/purl.js";
4
+
5
+ /**
6
+ * Perl parser for `cpanfile` and `cpanfile.snapshot`.
7
+ *
8
+ * Carton snapshots the resolved install set as a text document: one entry per
9
+ * distribution, each carrying the `pathname` it was fetched from (which
10
+ * encodes the PAUSE author id), the packages it provides, and the
11
+ * requirements it was resolved against. The declared `cpanfile` lists the
12
+ * project's own requirements.
13
+ *
14
+ * Distributions are identified with the registered `cpan` purl type, whose
15
+ * rules require the author id as the namespace
16
+ * (`pkg:cpan/<AUTHOR>/<Distribution>@<version>`).
17
+ */
18
+
19
+ /**
20
+ * Parse a `cpanfile.snapshot` into distributions with a dependency graph.
21
+ *
22
+ * @param {string} snapshotFile Path to `cpanfile.snapshot`
23
+ * @param {string} [cpanfileFile] Path to `cpanfile`, if present
24
+ * @returns {{ pkgList: object[], dependencies: object[] }}
25
+ */
26
+ export function parseCpanSnapshot(snapshotFile, cpanfileFile) {
27
+ let text;
28
+ try {
29
+ text = readFileSync(snapshotFile, "utf-8");
30
+ } catch (error) {
31
+ console.warn(`Failed to read ${snapshotFile}: ${error.message}`);
32
+ return { pkgList: [], dependencies: [] };
33
+ }
34
+
35
+ const distributions = [];
36
+ let current;
37
+ let section;
38
+ for (const rawLine of text.split("\n")) {
39
+ if (!rawLine.trim() || rawLine.trim().startsWith("#")) {
40
+ continue;
41
+ }
42
+ const indent = rawLine.length - rawLine.trimStart().length;
43
+ const line = rawLine.trim();
44
+ if (indent === 0) {
45
+ // Top-level headers separate the snapshot sections.
46
+ current = undefined;
47
+ section = line === "DISTRIBUTIONS" ? "distributions" : undefined;
48
+ continue;
49
+ }
50
+ if (line.startsWith("DISTRIBUTIONS")) {
51
+ section = "distributions";
52
+ continue;
53
+ }
54
+ if (section !== "distributions") {
55
+ continue;
56
+ }
57
+ // A distribution line is indented by two spaces and carries
58
+ // `<Name>-<version>`; its details follow indented deeper.
59
+ if (indent === 2) {
60
+ const dist = parseDistributionLine(line);
61
+ if (dist) {
62
+ distributions.push(dist);
63
+ current = dist;
64
+ }
65
+ continue;
66
+ }
67
+ if (!current) {
68
+ continue;
69
+ }
70
+ if (line.startsWith("pathname:")) {
71
+ current.author = authorFromPathname(
72
+ line.slice("pathname:".length).trim(),
73
+ );
74
+ continue;
75
+ }
76
+ if (line.startsWith("provides:")) {
77
+ current.inProvides = true;
78
+ current.inRequirements = false;
79
+ continue;
80
+ }
81
+ if (line.startsWith("requirements:")) {
82
+ current.inRequirements = true;
83
+ current.inProvides = false;
84
+ continue;
85
+ }
86
+ // Deeper indented `key value` lines under provides/requirements.
87
+ const pair = line.match(/^([A-Za-z0-9_:.-]+)\s+(\S+)$/u);
88
+ if (pair) {
89
+ if (current.inProvides) {
90
+ current.provides.set(pair[1], pair[2]);
91
+ } else if (current.inRequirements) {
92
+ current.requirements.push(pair[1]);
93
+ }
94
+ }
95
+ }
96
+
97
+ const directNames = cpanfileFile
98
+ ? directRequirementsFromCpanfile(cpanfileFile)
99
+ : new Set();
100
+
101
+ // Map every provided package name to its distribution so requirements can
102
+ // be resolved to edges between distributions.
103
+ const distByPackage = new Map();
104
+ for (const dist of distributions) {
105
+ for (const provided of dist.provides.keys()) {
106
+ if (!distByPackage.has(provided)) {
107
+ distByPackage.set(provided, dist);
108
+ }
109
+ }
110
+ }
111
+
112
+ const pkgList = [];
113
+ const refByDist = new Map();
114
+ for (const dist of distributions) {
115
+ const name = dist.name;
116
+ const version = dist.version;
117
+ const direct =
118
+ directNames.size === 0
119
+ ? true
120
+ : [...dist.provides.keys()].some((pkg) => directNames.has(pkg));
121
+ const pkg = cpanPackage(name, version, dist.author, {
122
+ direct,
123
+ srcFile: snapshotFile,
124
+ });
125
+ pkgList.push(pkg);
126
+ refByDist.set(dist, pkg["bom-ref"]);
127
+ }
128
+
129
+ const dependencies = [];
130
+ for (const dist of distributions) {
131
+ const ref = refByDist.get(dist);
132
+ const dependsOn = new Set();
133
+ for (const requirement of dist.requirements) {
134
+ const target = distByPackage.get(requirement);
135
+ if (target && target !== dist) {
136
+ const targetRef = refByDist.get(target);
137
+ if (targetRef) {
138
+ dependsOn.add(targetRef);
139
+ }
140
+ }
141
+ }
142
+ dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
143
+ }
144
+ return { pkgList, dependencies };
145
+ }
146
+
147
+ /**
148
+ * Parse a `Name-Version` distribution line.
149
+ *
150
+ * @param {string} line Trimmed distribution line
151
+ * @param {string} srcFile Source path for provenance
152
+ * @returns {object|undefined} Distribution accumulator
153
+ */
154
+ function parseDistributionLine(line) {
155
+ const match = line.match(/^([A-Za-z0-9_.-]+?)-(\d[^ ]*)$/u);
156
+ if (!match) {
157
+ return undefined;
158
+ }
159
+ return {
160
+ name: match[1],
161
+ version: match[2],
162
+ author: undefined,
163
+ provides: new Map(),
164
+ requirements: [],
165
+ inProvides: false,
166
+ inRequirements: false,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Extract the PAUSE author id from a CPAN pathname.
172
+ *
173
+ * Pathnames look like `M/MI/MIYAGAWA/Plack-1.0051.tar.gz`; the author id is
174
+ * the third segment.
175
+ *
176
+ * @param {string} pathname CPAN pathname
177
+ * @returns {string|undefined} Author id
178
+ */
179
+ function authorFromPathname(pathname) {
180
+ const segments = pathname.split("/").filter(Boolean);
181
+ return segments.length >= 3 ? segments[2] : undefined;
182
+ }
183
+
184
+ /**
185
+ * Build a component-like package record for a CPAN distribution.
186
+ *
187
+ * @param {string} name Distribution name
188
+ * @param {string|undefined} version Distribution version
189
+ * @param {string|undefined} author PAUSE author id
190
+ * @param {object} opts Extra context (`direct`, `srcFile`)
191
+ * @returns {object} Package record
192
+ */
193
+ function cpanPackage(name, version, author, opts) {
194
+ // The cpan purl type requires the author namespace; without it the purl is
195
+ // dropped rather than emitted without its identity anchor.
196
+ const purl = author
197
+ ? tryBuildPurl({
198
+ type: "cpan",
199
+ namespace: author,
200
+ name,
201
+ version: version || undefined,
202
+ })
203
+ : null;
204
+ const properties = [
205
+ { name: "internal:SrcFile", value: opts.srcFile },
206
+ {
207
+ name: "cdx:cpan:dependency",
208
+ value: opts.direct ? "direct" : "transitive",
209
+ },
210
+ ];
211
+ const pkg = {
212
+ name,
213
+ ...(version ? { version } : {}),
214
+ type: "library",
215
+ scope: "required",
216
+ properties,
217
+ };
218
+ if (purl) {
219
+ pkg.purl = purl;
220
+ pkg["bom-ref"] = decodeURIComponent(purl);
221
+ } else {
222
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
223
+ }
224
+ return pkg;
225
+ }
226
+
227
+ /**
228
+ * Collect the requirement names declared in a `cpanfile`.
229
+ *
230
+ * Only the `requires` keyword outside phase blocks is read; phases such as
231
+ * `on 'test'` are ignored so test-only modules do not become direct
232
+ * dependencies.
233
+ *
234
+ * @param {string} cpanfileFile Path to `cpanfile`
235
+ * @returns {Set<string>} Declared requirement names
236
+ */
237
+ export function directRequirementsFromCpanfile(cpanfileFile) {
238
+ const names = new Set();
239
+ let text;
240
+ try {
241
+ text = readFileSync(cpanfileFile, "utf-8");
242
+ } catch (error) {
243
+ console.warn(`Failed to read ${cpanfileFile}: ${error.message}`);
244
+ return names;
245
+ }
246
+ let inPhaseBlock = false;
247
+ let blockDepth = 0;
248
+ for (const rawLine of text.split("\n")) {
249
+ const line = rawLine.trim();
250
+ if (!line || line.startsWith("#")) {
251
+ continue;
252
+ }
253
+ if (inPhaseBlock) {
254
+ for (const char of line) {
255
+ if (char === "{") {
256
+ blockDepth += 1;
257
+ } else if (char === "}") {
258
+ blockDepth -= 1;
259
+ if (blockDepth === 0) {
260
+ inPhaseBlock = false;
261
+ }
262
+ }
263
+ }
264
+ continue;
265
+ }
266
+ if (/^on\s+['"]/.test(line) && line.includes("sub")) {
267
+ inPhaseBlock = true;
268
+ blockDepth = line.includes("{") ? 1 : 0;
269
+ continue;
270
+ }
271
+ const requires = line.match(/^requires\s+['"]([^'"]+)['"]/u);
272
+ if (requires) {
273
+ names.add(requires[1]);
274
+ }
275
+ }
276
+ return names;
277
+ }