@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,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
+ }
@@ -4,10 +4,11 @@ import process from "node:process";
4
4
  import { URL } from "node:url";
5
5
 
6
6
  import { build } from "@cdxgen/cdx-purl";
7
- import toml from "@iarna/toml";
7
+ import * as toml from "smol-toml";
8
8
  import { parse as _load } from "yaml";
9
9
 
10
10
  import { DEBUG_MODE } from "../core/activity.js";
11
+ import { recordDegradation } from "../core/buildLedger.js";
11
12
  import { getAllFiles, safeExistsSync, safeSpawnSync } from "../core/fs.js";
12
13
  import { PYTHON_STD_MODULES } from "../core/state.js";
13
14
  import { tryBuildPurl } from "../inventory/purl.js";
@@ -27,6 +28,41 @@ import {
27
28
  normalizePyLockRegistry,
28
29
  } from "./pylockutils.js";
29
30
 
31
+ /**
32
+ * TOML lock files a specific Python manager owns, mapped to the manager and
33
+ * the remediation id that ranks its repair. The failing file itself is the
34
+ * detection: the lock command that regenerates it belongs to the manager
35
+ * that wrote it.
36
+ *
37
+ * @type {Record<string, {manager: string, remediationId: string}>}
38
+ */
39
+ const PY_LOCKFILE_DEGRADATIONS = {
40
+ "uv.lock": {
41
+ manager: "uv",
42
+ remediationId: "python.lockfile-unparseable.uv",
43
+ },
44
+ "poetry.lock": {
45
+ manager: "poetry",
46
+ remediationId: "python.lockfile-unparseable.poetry",
47
+ },
48
+ "pdm.lock": {
49
+ manager: "pdm",
50
+ remediationId: "python.lockfile-unparseable.pdm",
51
+ },
52
+ };
53
+
54
+ /**
55
+ * The repair for a TOML lock file that names no single manager, such as
56
+ * PEP 751's `pylock.toml`. Its lock command carries the manager variable, so
57
+ * the shaping fills in whichever manager the project turns out to use.
58
+ *
59
+ * @type {{manager: string, remediationId: string}}
60
+ */
61
+ const GENERIC_PY_LOCKFILE_DEGRADATION = {
62
+ manager: "python",
63
+ remediationId: "python.lockfile-unparseable",
64
+ };
65
+
30
66
  /**
31
67
  * Method to parse python requires_dist attribute found in pypi setup.py
32
68
  *
@@ -444,7 +480,12 @@ export function parsePyProjectTomlFile(tomlFile) {
444
480
  for (const agroup of Object.keys(tomlData["dependency-groups"])) {
445
481
  tomlData["dependency-groups"][agroup].forEach((p) => {
446
482
  if (typeof p === "string" || p instanceof String) {
447
- const pname = p.split(/(==|<=|~=|>=)/)[0].split(" ")[0];
483
+ const pname = normalizePythonDependencyKey(
484
+ p.split(/(==|<=|~=|>=)/)[0].split(" ")[0],
485
+ );
486
+ if (!pname) {
487
+ return;
488
+ }
448
489
  if (!groupDepsKeys[pname]) {
449
490
  groupDepsKeys[pname] = [];
450
491
  }
@@ -466,8 +507,13 @@ export function parsePyProjectTomlFile(tomlFile) {
466
507
  }
467
508
  if (tomlData?.tool?.poetry?.dependencies) {
468
509
  for (const adep of Object.keys(tomlData?.tool?.poetry?.dependencies)) {
510
+ // poetry keys keep the author's casing (Click = "^8.1"), while every
511
+ // consumer of directDepsKeys looks entries up with the normalised
512
+ // (lowercase, hyphenated) package name, so normalise at the source.
513
+ const poetryDepKey = normalizePythonDependencyKey(adep);
469
514
  if (
470
- ![
515
+ !poetryDepKey ||
516
+ [
471
517
  "python",
472
518
  "py",
473
519
  "pytest",
@@ -475,43 +521,50 @@ export function parsePyProjectTomlFile(tomlFile) {
475
521
  "ruff",
476
522
  "setuptools",
477
523
  "bandit",
478
- ].includes(adep)
524
+ ].includes(poetryDepKey)
479
525
  ) {
480
- directDepsKeys[adep] = true;
481
- const poetryDependency = tomlData.tool.poetry.dependencies[adep];
482
- if (poetryDependency?.git) {
483
- recordPythonDependencySource(
484
- dependencySourceMap,
485
- adep,
486
- "git",
487
- poetryDependency.git,
488
- );
489
- } else if (poetryDependency?.url) {
490
- recordPythonDependencySource(
491
- dependencySourceMap,
492
- adep,
493
- "url",
494
- poetryDependency.url,
495
- );
496
- } else if (poetryDependency?.path) {
497
- recordPythonDependencySource(
498
- dependencySourceMap,
499
- adep,
500
- "path",
501
- poetryDependency.path,
502
- );
503
- }
526
+ continue;
527
+ }
528
+ directDepsKeys[poetryDepKey] = true;
529
+ const poetryDependency = tomlData.tool.poetry.dependencies[adep];
530
+ if (poetryDependency?.git) {
531
+ recordPythonDependencySource(
532
+ dependencySourceMap,
533
+ adep,
534
+ "git",
535
+ poetryDependency.git,
536
+ );
537
+ } else if (poetryDependency?.url) {
538
+ recordPythonDependencySource(
539
+ dependencySourceMap,
540
+ adep,
541
+ "url",
542
+ poetryDependency.url,
543
+ );
544
+ } else if (poetryDependency?.path) {
545
+ recordPythonDependencySource(
546
+ dependencySourceMap,
547
+ adep,
548
+ "path",
549
+ poetryDependency.path,
550
+ );
504
551
  }
505
552
  } // for
553
+ // Group keys are matched against the normalised package names from the
554
+ // lock file, so they are normalised here too.
506
555
  if (tomlData?.tool?.poetry?.group) {
507
556
  for (const agroup of Object.keys(tomlData.tool.poetry.group)) {
508
557
  for (const adep of Object.keys(
509
558
  tomlData.tool.poetry.group[agroup]?.dependencies,
510
559
  )) {
511
- if (!groupDepsKeys[adep]) {
512
- groupDepsKeys[adep] = [];
560
+ const poetryGroupDepKey = normalizePythonDependencyKey(adep);
561
+ if (!poetryGroupDepKey) {
562
+ continue;
513
563
  }
514
- groupDepsKeys[adep].push(agroup);
564
+ if (!groupDepsKeys[poetryGroupDepKey]) {
565
+ groupDepsKeys[poetryGroupDepKey] = [];
566
+ }
567
+ groupDepsKeys[poetryGroupDepKey].push(agroup);
515
568
  const poetryDependency =
516
569
  tomlData.tool.poetry.group[agroup]?.dependencies?.[adep];
517
570
  if (poetryDependency?.git) {
@@ -780,7 +833,27 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
780
833
  dependencySourceMap = pyProjMap.dependencySourceMap || {};
781
834
  parentComponent = pyProjMap.parentComponent;
782
835
  workspacePaths = pyProjMap.workspacePaths;
783
- if (workspacePaths?.length) {
836
+ // `parsePyProjectTomlFile` reports a parse failure and returns an empty map, so a
837
+ // pyproject.toml this parser cannot read leaves `parentComponent` undefined while
838
+ // the lock file still declares workspaces. Building the workspace tree then throws
839
+ // on the first dereference and takes the whole SBOM with it. Degrade to a flattened
840
+ // tree instead - the caller supplies its own parent component, so the components
841
+ // themselves are still reported.
842
+ if (workspacePaths?.length && !parentComponent) {
843
+ console.log(
844
+ `Unable to read a parent component from ${pyProjectFile}, so the workspace structure declared in ${basename(lockFile)} cannot be reconstructed.`,
845
+ );
846
+ console.log(
847
+ "The dependency tree in the generated SBOM will be flattened and therefore incorrect.",
848
+ );
849
+ recordDegradation("python.pyproject-unparseable", {
850
+ ecosystem: "python",
851
+ impact: "transitive-deps",
852
+ path: pyProjectFile,
853
+ detail: `${basename(pyProjectFile)} could not be parsed, so the workspace structure declared in ${basename(lockFile)} was not reconstructed.`,
854
+ });
855
+ }
856
+ if (workspacePaths?.length && parentComponent) {
784
857
  if (!hasWorkspaces) {
785
858
  hasWorkspaces = true;
786
859
  }
@@ -840,7 +913,12 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
840
913
  ] = awpyproj;
841
914
  }
842
915
  }
843
- const wparentComponentRef = wcompMap.parentComponent["bom-ref"];
916
+ // The block above already treats an unreadable workspace pyproject.toml as
917
+ // tolerable, so this must not dereference the component it skipped. A
918
+ // workspace member whose manifest cannot be parsed contributes no parent
919
+ // ref, and recording `undefined` as the parent of its dependencies would
920
+ // corrupt the tree for the members that did parse.
921
+ const wparentComponentRef = wcompMap?.parentComponent?.["bom-ref"];
844
922
  if (wcompMap?.dependencySourceMap) {
845
923
  Object.assign(
846
924
  workspaceDependencySourceMap,
@@ -848,7 +926,7 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
848
926
  );
849
927
  }
850
928
  // Track the parents of workspace direct dependencies
851
- if (wcompMap?.directDepsKeys) {
929
+ if (wcompMap?.directDepsKeys && wparentComponentRef) {
852
930
  for (const wdd of Object.keys(wcompMap?.directDepsKeys)) {
853
931
  if (!pkgParentMap[wdd]) {
854
932
  pkgParentMap[wdd] = [];
@@ -865,6 +943,20 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
865
943
  lockTomlObj = toml.parse(lockData);
866
944
  } catch (err) {
867
945
  if (lockFile) {
946
+ // The failing file names the manager whose lock command regenerates
947
+ // it. A standards-named lock file (PEP 751 `pylock.toml`) belongs to
948
+ // no single manager, so it falls back to the generic repair, whose
949
+ // lock command the shaping resolves from whichever manager the
950
+ // project turns out to use.
951
+ const degradation =
952
+ PY_LOCKFILE_DEGRADATIONS[basename(lockFile)] ||
953
+ GENERIC_PY_LOCKFILE_DEGRADATION;
954
+ recordDegradation(degradation.remediationId, {
955
+ ecosystem: "python",
956
+ impact: "transitive-deps",
957
+ path: lockFile,
958
+ detail: `The ${degradation.manager} lock file could not be parsed, so no locked versions were captured from it.`,
959
+ });
868
960
  console.log(`Error while parsing the lock file ${lockFile}.`, err);
869
961
  } else {
870
962
  console.log("Error while parsing the lock data as toml", err);
@@ -1218,6 +1310,38 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
1218
1310
  }
1219
1311
  }
1220
1312
  }
1313
+ // Seed the parent component's first-level edges from the manifest. Lock
1314
+ // files describe the resolved closure but do not say which of the locked
1315
+ // packages the root project directly depends on; parsePyProjectTomlFile
1316
+ // derived that set into directDepsKeys from [project.dependencies] /
1317
+ // [tool.poetry.dependencies]. Only packages that are locked resolve to a
1318
+ // bom-ref here, so the seeded edge never dangles and dev-group or
1319
+ // unresolved dependencies stay out of the first level. Seeding
1320
+ // depsMap (instead of pushing into dependenciesList) lets the
1321
+ // materialisation below apply its usual ref resolution and sorting.
1322
+ if (parentComponent?.["bom-ref"] && Object.keys(directDepsKeys).length) {
1323
+ const rootRef = parentComponent["bom-ref"];
1324
+ const rootDeps = new Set();
1325
+ for (const depKey of Object.keys(directDepsKeys)) {
1326
+ const depRef =
1327
+ existingPkgMap[depKey] ||
1328
+ existingPkgMap[depKey.replaceAll("-", "_")] ||
1329
+ existingPkgMap[`py${depKey}`];
1330
+ if (depRef && depRef !== rootRef) {
1331
+ rootDeps.add(depRef);
1332
+ }
1333
+ }
1334
+ if (rootDeps.size) {
1335
+ const existingRootDeps = depsMap[rootRef];
1336
+ if (existingRootDeps) {
1337
+ for (const adep of rootDeps) {
1338
+ existingRootDeps.add(adep);
1339
+ }
1340
+ } else {
1341
+ depsMap[rootRef] = rootDeps;
1342
+ }
1343
+ }
1344
+ }
1221
1345
  for (const key of Object.keys(depsMap)) {
1222
1346
  const dependsOnList = new Set();
1223
1347
  const parentPkg = pkgBomRefMap[key];
@@ -1921,8 +2045,20 @@ export function parsePixiTomlFile(pixiToml) {
1921
2045
  pkg.name = projectData.name;
1922
2046
  pkg.version = projectData.version;
1923
2047
  // pkg.authors = tomlData['project']['authors'];
1924
- pkg.homepage = projectData.homepage;
1925
- pkg.repository = projectData.repository;
2048
+ // The parent component only gets `cleanParentComponent`, which converts the
2049
+ // transient `{ url }` shape — raw strings would be dropped silently, so
2050
+ // emit external references directly instead.
2051
+ if (projectData.homepage) {
2052
+ pkg.externalReferences = pkg.externalReferences || [];
2053
+ pkg.externalReferences.push({
2054
+ type: "website",
2055
+ url: projectData.homepage,
2056
+ });
2057
+ }
2058
+ if (projectData.repository) {
2059
+ pkg.externalReferences = pkg.externalReferences || [];
2060
+ pkg.externalReferences.push({ type: "vcs", url: projectData.repository });
2061
+ }
1926
2062
  return pkg;
1927
2063
  }
1928
2064