@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
@@ -2,8 +2,10 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
 
4
4
  import { build, Purl } from "@cdxgen/cdx-purl";
5
+ import { clean } from "semver";
5
6
 
6
7
  import { safeExistsSync } from "../core/fs.js";
8
+ import { parsePurlLoose } from "../inventory/purl.js";
7
9
  import { parseNpmrc, parseNpmrcFromEnv } from "../parsers/npmrc.js";
8
10
 
9
11
  const npmPackageHydrationFields = [
@@ -84,6 +86,89 @@ export function setNpmPeerProperty(pkg) {
84
86
  addComponentProperty(pkg, "cdx:npm:package:peer", "true");
85
87
  }
86
88
 
89
+ /**
90
+ * Marks an npm component as imported only for its TypeScript types.
91
+ *
92
+ * @param {object} pkg Component object to annotate
93
+ * @returns {void}
94
+ */
95
+ export function setNpmTypeOnlyProperty(pkg) {
96
+ addComponentProperty(pkg, "cdx:npm:package:type-only", "true");
97
+ }
98
+
99
+ /**
100
+ * Clean an npm version the way `npm publish` does before it records a release.
101
+ *
102
+ * Publishing a directory runs the manifest through `npm pkg fix`
103
+ * (`fixVersionField` in `@npmcli/package-json`, which calls `semver.clean` in
104
+ * loose mode) and then through libnpmpublish, which calls `semver.clean` again
105
+ * in strict mode and records the result as the packument version, the
106
+ * dist-tag, the `_id` and the tarball file name. Semver build metadata
107
+ * (`1.0.0+build.5`), a leading `v` or `=`, surrounding whitespace and leading
108
+ * zeros are dropped; a prerelease is kept.
109
+ *
110
+ * The `package.json` inside the published tarball keeps the original string,
111
+ * and so do the lockfiles npm, pnpm, yarn and bun write for a project, a
112
+ * workspace member or a git, file or link dependency. The cleaned form is the
113
+ * version the npm registry knows the package by.
114
+ *
115
+ * @param {string} version Version as a manifest or lockfile records it
116
+ * @returns {string|null} The cleaned version, or null when npm would reject `version` as invalid semver
117
+ */
118
+ export function cleanNpmVersion(version) {
119
+ if (typeof version !== "string" || !version.trim()) {
120
+ return null;
121
+ }
122
+ const looseClean = clean(version, { loose: true });
123
+ if (!looseClean) {
124
+ return null;
125
+ }
126
+ return clean(looseClean);
127
+ }
128
+
129
+ /**
130
+ * Record the version npm would publish when it differs from the recorded one.
131
+ *
132
+ * Sets `cdx:npm:cleanedVersion` on an npm component whose version is valid
133
+ * semver but not in the form {@link cleanNpmVersion} produces, for example
134
+ * `1.0.0+build.5` (published as `1.0.0`). The component's `version` and `purl`
135
+ * keep the recorded string: that is what the manifest and lockfile say. A
136
+ * component whose version is already clean, or not semver at all (a git
137
+ * commit, a `file:` path), is left without the property, and a stale value from
138
+ * an earlier pass is replaced.
139
+ *
140
+ * @param {object} component Component or package object with `version` and `purl`
141
+ * @returns {object} The same component, for chaining
142
+ */
143
+ export function setNpmCleanedVersionProperty(component) {
144
+ if (!component || typeof component !== "object") {
145
+ return component;
146
+ }
147
+ const purl = component.purl;
148
+ if (typeof purl !== "string" || !purl.startsWith("pkg:npm/")) {
149
+ return component;
150
+ }
151
+ const cleaned = cleanNpmVersion(component.version);
152
+ const wanted = cleaned && cleaned !== component.version ? cleaned : undefined;
153
+ const existing = component.properties?.filter(
154
+ (property) => property?.name === "cdx:npm:cleanedVersion",
155
+ );
156
+ if (
157
+ (!existing?.length && !wanted) ||
158
+ (existing?.length === 1 && existing[0].value === wanted)
159
+ ) {
160
+ return component;
161
+ }
162
+ const properties = (component.properties || []).filter(
163
+ (property) => property?.name !== "cdx:npm:cleanedVersion",
164
+ );
165
+ if (wanted) {
166
+ properties.push({ name: "cdx:npm:cleanedVersion", value: wanted });
167
+ }
168
+ component.properties = properties;
169
+ return component;
170
+ }
171
+
87
172
  /**
88
173
  * Helper function to create a properly encoded workspace PURL
89
174
  *
@@ -130,14 +215,18 @@ export function findMatchingNpmWorkspace(workspacePackages, packageName) {
130
215
  return undefined;
131
216
  }
132
217
 
133
- const expectedEncodedPurl = createNpmWorkspacePurl(packageName);
134
- const simplePurl = `pkg:npm/${packageName}`;
135
-
136
- return workspacePackages.find(
137
- (workspacePackage) =>
138
- workspacePackage.startsWith(expectedEncodedPurl) ||
139
- workspacePackage.startsWith(simplePurl),
140
- );
218
+ // Compare the parsed coordinates: a prefix match on the purl text would let
219
+ // `ws` match `wsmember`, and the reference may be escaped or decoded.
220
+ return workspacePackages.find((workspacePackage) => {
221
+ const parts = parsePurlLoose(workspacePackage);
222
+ if (parts?.type !== "npm") {
223
+ return false;
224
+ }
225
+ const fullName = parts.namespace
226
+ ? `${parts.namespace}/${parts.name}`
227
+ : parts.name;
228
+ return fullName === packageName;
229
+ });
141
230
  }
142
231
 
143
232
  /**
@@ -201,6 +290,12 @@ export function collectNpmManifestSources(node) {
201
290
  return manifestSources;
202
291
  }
203
292
  for (const edge of node.edgesIn) {
293
+ // Arborist adds a `workspace` edge from the root to every member, with an
294
+ // absolute `file:` spec it builds from the checkout path. No manifest
295
+ // declares it, and copying it would put the local path into the BOM.
296
+ if (edge?.workspace || edge?.type === "workspace") {
297
+ continue;
298
+ }
204
299
  const manifestSource = classifyNpmManifestSource(edge?.spec);
205
300
  if (!manifestSource) {
206
301
  continue;
@@ -520,7 +615,9 @@ export function parsePnpmGitLockKey(lockKey) {
520
615
  * @returns {Object<string, object>} Map of lookup key to git package reference.
521
616
  */
522
617
  export function buildPnpmGitPkgRefs(packages, snapshots, npmrcConfig = {}) {
523
- const gitPkgRefs = {};
618
+ // A null prototype keeps inherited Object properties (such as `constructor`)
619
+ // from masquerading as lookup hits for keys that were never registered.
620
+ const gitPkgRefs = Object.create(null);
524
621
  const registerEntry = (lockKey, packageNode) => {
525
622
  const resolution = packageNode?.resolution;
526
623
  const parsed = parsePnpmGitLockKey(lockKey);
@@ -531,6 +628,10 @@ export function buildPnpmGitPkgRefs(packages, snapshots, npmrcConfig = {}) {
531
628
  return;
532
629
  }
533
630
  const { group, name, gitSpec, fullName, packageName } = parsed;
631
+ if (name === "__proto__" || packageName === "__proto__") {
632
+ // A crafted lock key cannot become a lookup entry under this name.
633
+ return;
634
+ }
534
635
  const version = packageNode?.version || resolution?.commit || "";
535
636
  const repo = resolution?.repo || "";
536
637
  const commit = resolution?.commit || "";
@@ -0,0 +1,179 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ import { parse as loadYaml } from "yaml";
4
+
5
+ import { tryBuildPurl } from "../inventory/purl.js";
6
+
7
+ /**
8
+ * Crystal parser for `shard.yml` and `shard.lock`.
9
+ *
10
+ * Shards resolves dependencies declared in `shard.yml` and records the
11
+ * resolved set in `shard.lock`, which pins each dependency's version and
12
+ * source repository. Both files are YAML and are read with the standard
13
+ * parser.
14
+ *
15
+ * No `crystal` purl type is registered, so shards are identified as generic
16
+ * packages carrying a `cdx:purl:proposedType=crystal` property, following
17
+ * the convention used for nix and zig. This keeps the BOM valid today and
18
+ * allows a mechanical migration if a type is registered upstream.
19
+ */
20
+
21
+ /**
22
+ * Parse a Crystal project from `shard.yml` and optional `shard.lock`.
23
+ *
24
+ * @param {string} shardYmlFile Path to `shard.yml`
25
+ * @param {string} [shardLockFile] Path to `shard.lock`, if present
26
+ * @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs?: string[] }}
27
+ */
28
+ export function parseShardsProject(shardYmlFile, shardLockFile) {
29
+ const manifest = readYamlFile(shardYmlFile);
30
+
31
+ const parentComponent = {};
32
+ if (typeof manifest?.name === "string" && manifest.name) {
33
+ parentComponent.type = "application";
34
+ parentComponent.name = manifest.name;
35
+ if (typeof manifest.version === "string" && manifest.version) {
36
+ parentComponent.version = manifest.version;
37
+ }
38
+ parentComponent.description =
39
+ typeof manifest.description === "string" && manifest.description
40
+ ? manifest.description
41
+ : `Crystal shard: ${manifest.name}`;
42
+ parentComponent.properties = [
43
+ { name: "internal:SrcFile", value: shardYmlFile },
44
+ ];
45
+ if (typeof manifest.crystal === "string" && manifest.crystal) {
46
+ parentComponent.properties.push({
47
+ name: "cdx:crystal:languageVersion",
48
+ value: manifest.crystal,
49
+ });
50
+ }
51
+ }
52
+
53
+ const declared =
54
+ manifest?.dependencies && typeof manifest.dependencies === "object"
55
+ ? Object.keys(manifest.dependencies)
56
+ : [];
57
+ const devDeclared =
58
+ manifest?.development_dependencies &&
59
+ typeof manifest.development_dependencies === "object"
60
+ ? Object.keys(manifest.development_dependencies)
61
+ : [];
62
+
63
+ let lock;
64
+ if (shardLockFile) {
65
+ lock = readYamlFile(shardLockFile);
66
+ }
67
+ const lockedShards =
68
+ lock?.shards && typeof lock.shards === "object" ? lock.shards : undefined;
69
+ if (!lockedShards) {
70
+ // Lockless fallback: emit declared dependencies without resolved versions.
71
+ const pkgList = [
72
+ ...declared.map((name) =>
73
+ crystalPackage(name, undefined, {
74
+ direct: true,
75
+ dev: false,
76
+ srcFile: shardYmlFile,
77
+ }),
78
+ ),
79
+ ...devDeclared.map((name) =>
80
+ crystalPackage(name, undefined, {
81
+ direct: true,
82
+ dev: true,
83
+ srcFile: shardYmlFile,
84
+ }),
85
+ ),
86
+ ];
87
+ return {
88
+ pkgList,
89
+ dependencies: [],
90
+ parentComponent,
91
+ rootInputs: pkgList.map((p) => p["bom-ref"]),
92
+ };
93
+ }
94
+
95
+ const directNames = new Set(declared);
96
+ const devNames = new Set(devDeclared);
97
+ const pkgList = [];
98
+ const refByName = new Map();
99
+ for (const [name, entry] of Object.entries(lockedShards)) {
100
+ const version =
101
+ typeof entry?.version === "string" && entry.version
102
+ ? entry.version
103
+ : undefined;
104
+ const pkg = crystalPackage(name, version, {
105
+ direct: directNames.has(name),
106
+ dev: devNames.has(name),
107
+ srcFile: shardLockFile,
108
+ });
109
+ pkgList.push(pkg);
110
+ refByName.set(name, pkg["bom-ref"]);
111
+ }
112
+
113
+ // shard.lock records no dependency graph between shards, so the root links
114
+ // to the declared set and every other shard stands on its own version pin.
115
+ const rootInputs = [...directNames, ...devNames]
116
+ .map((name) => refByName.get(name))
117
+ .filter(Boolean);
118
+ return { pkgList, dependencies: [], parentComponent, rootInputs };
119
+ }
120
+
121
+ /**
122
+ * Build a component-like package record for a Crystal shard.
123
+ *
124
+ * @param {string} name Shard name
125
+ * @param {string|undefined} version Resolved version, if known
126
+ * @param {object} opts Extra context (`direct`, `dev`, `srcFile`)
127
+ * @returns {object} Package record
128
+ */
129
+ function crystalPackage(name, version, opts) {
130
+ const purl = tryBuildPurl({
131
+ type: "generic",
132
+ name,
133
+ version: version || undefined,
134
+ });
135
+ const properties = [
136
+ { name: "internal:SrcFile", value: opts.srcFile },
137
+ { name: "cdx:purl:proposedType", value: "crystal" },
138
+ ];
139
+ if (opts.direct) {
140
+ properties.push({
141
+ name: "cdx:crystal:dependency",
142
+ value: opts.dev ? "direct-dev" : "direct",
143
+ });
144
+ } else {
145
+ properties.push({ name: "cdx:crystal:dependency", value: "transitive" });
146
+ }
147
+ if (opts.dev) {
148
+ properties.push({ name: "cdx:crystal:scope", value: "development" });
149
+ }
150
+ const pkg = {
151
+ name,
152
+ ...(version ? { version } : {}),
153
+ type: "library",
154
+ scope: opts.dev ? "optional" : "required",
155
+ properties,
156
+ };
157
+ if (purl) {
158
+ pkg.purl = purl;
159
+ pkg["bom-ref"] = decodeURIComponent(purl);
160
+ } else {
161
+ pkg["bom-ref"] = `library:${name}:${version || ""}`;
162
+ }
163
+ return pkg;
164
+ }
165
+
166
+ /**
167
+ * Read and parse a YAML file, warning instead of throwing.
168
+ *
169
+ * @param {string} filePath File to read
170
+ * @returns {object|undefined} Parsed value
171
+ */
172
+ function readYamlFile(filePath) {
173
+ try {
174
+ return loadYaml(readFileSync(filePath, "utf-8"));
175
+ } catch (error) {
176
+ console.warn(`Failed to parse ${filePath}: ${error.message}`);
177
+ return undefined;
178
+ }
179
+ }
@@ -13,6 +13,34 @@ import { findLicenseId } from "../inventory/spdx.js";
13
13
  import { xml2js } from "../parsers/xml.js";
14
14
  import { extractPackageInfoFromHintPath } from "./dotnetutils.js";
15
15
 
16
+ /**
17
+ * Apply a version to a NuGet component so that the `version` field and the purl
18
+ * always agree.
19
+ *
20
+ * A .NET manifest can state a version that is not a version: an MSBuild property
21
+ * such as `$(JsonVersion)`, a range such as `[3.13.3,4.0)`, a wildcard such as
22
+ * `2.0.*`, or nothing at all. Such a declaration cannot go into a purl, so a
23
+ * component that kept it in `version` while its purl carried none described one
24
+ * package two ways. A scanner range matching on the versionless purl then reports
25
+ * every advisory published against the package name.
26
+ *
27
+ * The declaration is not lost: callers record it as `cdx:nuget:declared_version_range`.
28
+ *
29
+ * @param {Object} pkg Component to update in place. `name` must already be set.
30
+ * @param {String} [version] Version as stated or resolved, concrete or not
31
+ *
32
+ * @returns {Object} The same component
33
+ */
34
+ export function applyNugetVersion(pkg, version) {
35
+ const concrete = concreteVersion(version);
36
+ if (concrete) {
37
+ pkg.version = concrete;
38
+ } else {
39
+ delete pkg.version;
40
+ }
41
+ return applyPurl(pkg, nugetPurl(pkg.name, concrete));
42
+ }
43
+
16
44
  /**
17
45
  * Method to parse .nupkg files
18
46
  *
@@ -140,16 +168,22 @@ export function parseNuspecData(nupkgFile, nuspecData) {
140
168
  continue;
141
169
  }
142
170
  groupPkg.name = agroupdep.id;
171
+ let declaredRange;
143
172
  if (agroupdep?.version) {
144
173
  let versionStr = agroupdep.version;
145
174
  // version could have square brackets around them
146
175
  if (versionStr.startsWith("[") && versionStr.endsWith("]")) {
147
176
  versionStr = versionStr.replace(/[\[\]]/g, "");
148
177
  }
149
- groupPkg.version = versionStr;
150
- applyPurl(groupPkg, nugetPurl(groupPkg.name, versionStr));
178
+ // A nuspec dependency states the range the package accepts, not the
179
+ // version installed. Anything that is not a single version stays a
180
+ // declaration so the component and its purl agree - #4359.
181
+ if (!concreteVersion(versionStr)) {
182
+ declaredRange = agroupdep.version;
183
+ }
184
+ applyNugetVersion(groupPkg, versionStr);
151
185
  } else {
152
- applyPurl(groupPkg, nugetPurl(groupPkg.name));
186
+ applyNugetVersion(groupPkg);
153
187
  }
154
188
  groupPkg.scope = "optional";
155
189
  groupPkg.properties = [
@@ -158,6 +192,12 @@ export function parseNuspecData(nupkgFile, nuspecData) {
158
192
  value: nupkgFile,
159
193
  },
160
194
  ];
195
+ if (declaredRange) {
196
+ groupPkg.properties.push({
197
+ name: "cdx:nuget:declared_version_range",
198
+ value: declaredRange,
199
+ });
200
+ }
161
201
  if (targetFramework) {
162
202
  groupPkg.properties.push({
163
203
  name: "cdx:dotnet:target_framework",
@@ -243,20 +283,15 @@ export function parseCsPkgData(pkgData, pkgFile, pkgNameVersions = {}) {
243
283
  // so the declaration stays visible, and the version is resolved from a
244
284
  // more precise manifest so the purl names the package that is present.
245
285
  let declaredRange;
246
- if (!pkg.version || pkg.version.includes("$(")) {
286
+ if (!concreteVersion(pkg.version)) {
247
287
  confidence = 0.5;
288
+ declaredRange = pkg.version;
248
289
  // Backfill from a version already resolved by a more precise manifest.
249
290
  // Concrete versions are never overridden - a disagreement means both
250
291
  // versions must be tracked.
251
- pkg.version = pkgNameVersions[pkg.name] || undefined;
252
- } else if (/[[\](),*]/.test(pkg.version)) {
253
- confidence = 0.5;
254
- if (pkgNameVersions[pkg.name]) {
255
- declaredRange = pkg.version;
256
- pkg.version = pkgNameVersions[pkg.name];
257
- }
292
+ pkg.version = pkgNameVersions[pkg.name];
258
293
  }
259
- applyPurl(pkg, nugetPurl(pkg.name, pkg.version));
294
+ applyNugetVersion(pkg, pkg.version);
260
295
  if (pkgFile) {
261
296
  pkg.properties = [
262
297
  {
@@ -431,6 +466,21 @@ export function getPropertyGroupTextNodes(propsFiles) {
431
466
  return matches;
432
467
  }
433
468
 
469
+ /**
470
+ * Read the trimmed text of an MSBuild property from a parsed PropertyGroup.
471
+ *
472
+ * @param {Object} propertyGroup PropertyGroup parsed by xml2js in compact mode
473
+ * @param {string} key Property name
474
+ * @returns {string|undefined} The text, or undefined when absent or empty
475
+ */
476
+ function propertyGroupText(propertyGroup, key) {
477
+ const text = propertyGroup?.[key]?.[0]?._?.[0];
478
+ if (typeof text !== "string" || !text.trim().length) {
479
+ return undefined;
480
+ }
481
+ return text.trim();
482
+ }
483
+
434
484
  /**
435
485
  * Method to parse .csproj like xml files
436
486
  *
@@ -479,6 +529,35 @@ export function parseCsProjData(
479
529
  return pkgList;
480
530
  }
481
531
  const project = projects[0];
532
+ // SDK-style projects declare their version through the MSBuild properties
533
+ // that `dotnet pack` reads: PackageVersion, which defaults to Version, which
534
+ // in turn defaults to VersionPrefix plus an optional `-VersionSuffix`. A
535
+ // property may be set in any PropertyGroup, so they are collected first.
536
+ const sdkVersionProps = {};
537
+ for (const apg of project?.PropertyGroup || []) {
538
+ for (const key of [
539
+ "PackageVersion",
540
+ "Version",
541
+ "VersionPrefix",
542
+ "VersionSuffix",
543
+ ]) {
544
+ const value = propertyGroupText(apg, key);
545
+ if (value !== undefined) {
546
+ sdkVersionProps[key] = value;
547
+ }
548
+ }
549
+ }
550
+ let sdkVersion =
551
+ sdkVersionProps.PackageVersion ||
552
+ sdkVersionProps.Version ||
553
+ (sdkVersionProps.VersionPrefix
554
+ ? `${sdkVersionProps.VersionPrefix}${sdkVersionProps.VersionSuffix ? `-${sdkVersionProps.VersionSuffix}` : ""}`
555
+ : undefined);
556
+ // An MSBuild property reference such as $(BuildNumber) is only known at
557
+ // build time, so a version that still contains one is not reported.
558
+ if (sdkVersion?.includes("$(")) {
559
+ sdkVersion = undefined;
560
+ }
482
561
  // First make up a parentcomponent name based on the .csproj file name
483
562
  if (projFile) {
484
563
  parentComponent.name = basename(projFile).replaceAll(
@@ -536,10 +615,6 @@ export function parseCsProjData(
536
615
  if (outputType === "Library") {
537
616
  parentComponent.type = "library";
538
617
  }
539
- applyPurl(
540
- parentComponent,
541
- nugetPurl(parentComponent.name, parentComponent.version || "latest"),
542
- );
543
618
  // The MSBuild OutputType (Exe, WinExe, …) used to ride along as an
544
619
  // `output_type` purl qualifier, which nuget does not define — every such
545
620
  // purl was invalid. It is project metadata, so it belongs in properties.
@@ -712,23 +787,17 @@ export function parseCsProjData(
712
787
  const exactPin = pkg.version.match(/^\[([^,[\]()]+)\]$/);
713
788
  if (exactPin) {
714
789
  pkg.version = exactPin[1];
715
- } else if (
716
- pkg.version.includes("$(") ||
717
- /[*[\](),]/.test(pkg.version)
718
- ) {
719
- const resolved =
790
+ } else if (!concreteVersion(pkg.version)) {
791
+ declaredRange = pkg.version;
792
+ pkg.version =
720
793
  pkgNameVersions[pkg.name] ||
721
794
  centralVersions[pkg.name.toLowerCase()];
722
- if (resolved) {
723
- declaredRange = pkg.version;
724
- pkg.version = resolved;
725
- }
726
795
  }
727
796
  }
728
797
  // A version that could not be resolved from any source must not be
729
798
  // stringified into the purl - `@undefined` is not a version and makes the
730
799
  // component unmatchable. Mirrors parseCsPkgData.
731
- applyPurl(pkg, nugetPurl(pkg.name, pkg.version));
800
+ applyNugetVersion(pkg, pkg.version);
732
801
  if (projFile) {
733
802
  pkg.properties = [
734
803
  {
@@ -821,26 +890,27 @@ export function parseCsProjData(
821
890
  });
822
891
  }
823
892
  const version = hintVersion ?? assemblyVersion;
824
- if (version) {
825
- pkg.version = version;
826
- applyPurl(pkg, nugetPurl(pkg.name, version));
827
- } else {
828
- applyPurl(pkg, nugetPurl(pkg.name));
829
- if (
830
- pkg.name.startsWith("System.") ||
893
+ // An assembly reference that states no version anywhere - no HintPath to
894
+ // read one from and no `Version=` on the Include - leaves the component
895
+ // versionless in both fields. The registry's latest release is not a
896
+ // stand-in: `<Reference>` resolves an assembly from a targeting pack, a
897
+ // lib folder or the GAC, and the project does not say which package it
898
+ // came from. See #4359.
899
+ applyNugetVersion(pkg, version);
900
+ if (
901
+ !pkg.version &&
902
+ (pkg.name.startsWith("System.") ||
831
903
  pkg.name.startsWith("Mono.") ||
832
- pkg.name.startsWith("Microsoft.")
833
- ) {
834
- // If this is a System package, then track the target frameworks
835
- for (const tf of projectTargetFrameworks) {
836
- pkg.properties.push({
837
- name: "cdx:dotnet:target_framework",
838
- value: tf,
839
- });
840
- }
904
+ pkg.name.startsWith("Microsoft."))
905
+ ) {
906
+ // If this is a System package, then track the target frameworks
907
+ for (const tf of projectTargetFrameworks) {
908
+ pkg.properties.push({
909
+ name: "cdx:dotnet:target_framework",
910
+ value: tf,
911
+ });
841
912
  }
842
913
  }
843
- pkg["bom-ref"] = pkg.purl;
844
914
  if (assemblyName) {
845
915
  pkg.properties.push({
846
916
  name: "cdx:dotnet:assembly_name",
@@ -894,21 +964,22 @@ export function parseCsProjData(
894
964
  }
895
965
  }
896
966
  }
897
- // If the parent still lacks a purl, add one based on the name and version
898
- if (parentComponent && !parentComponent.purl && parentComponent.name) {
967
+ if (sdkVersion) {
968
+ parentComponent.version = sdkVersion;
969
+ }
970
+ // The purl is built once every PropertyGroup has been read, so a version
971
+ // declared after OutputType is not lost.
972
+ if (parentComponent.name) {
899
973
  applyPurl(
900
974
  parentComponent,
901
975
  nugetPurl(parentComponent.name, parentComponent.version || "latest"),
902
976
  );
903
977
  }
904
- if (parentComponent?.purl) {
905
- parentComponent["bom-ref"] = parentComponent.purl;
906
- }
907
978
  let dependencies = [];
908
979
  if (parentComponent?.["bom-ref"]) {
909
980
  dependencies = [
910
981
  {
911
- ref: parentComponent.purl,
982
+ ref: parentComponent["bom-ref"],
912
983
  dependsOn: [...new Set(pkgList.map((p) => p["bom-ref"]))].sort(),
913
984
  },
914
985
  ];