@cdxgen/cdxgen 13.3.0 → 13.4.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 (255) hide show
  1. package/README.md +9 -6
  2. package/bin/cdxgen.js +90 -26
  3. package/bin/evinse.js +18 -1
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +45 -15
  6. package/bin/tracebom.js +5 -1
  7. package/bin/validate.js +84 -18
  8. package/bin/verify.js +155 -86
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +61 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/index.js +113 -54
  15. package/lib/cli/jsBom.js +50 -29
  16. package/lib/cli/jvmBom.js +665 -138
  17. package/lib/cli/managedBom.js +320 -200
  18. package/lib/cli/nativeBom.js +139 -30
  19. package/lib/core/activity.js +12 -0
  20. package/lib/core/env.js +17 -9
  21. package/lib/core/fs.js +3 -2
  22. package/lib/core/httpClient.js +159 -24
  23. package/lib/core/pluginRun.js +184 -0
  24. package/lib/core/processTree.js +100 -0
  25. package/lib/core/runState.js +39 -0
  26. package/lib/core/toml.js +72 -0
  27. package/lib/ecosystems/asarutils.js +4 -2
  28. package/lib/ecosystems/bunutils.js +5 -7
  29. package/lib/ecosystems/cargoCache.js +231 -8
  30. package/lib/ecosystems/cmakeResolver.js +10 -21
  31. package/lib/ecosystems/cppBuildContext.js +631 -0
  32. package/lib/ecosystems/cppEvidence.js +337 -22
  33. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  34. package/lib/ecosystems/denoutils.js +93 -66
  35. package/lib/ecosystems/ecosystems.js +1967 -788
  36. package/lib/ecosystems/golem.js +28 -12
  37. package/lib/ecosystems/gradleutils.js +314 -30
  38. package/lib/ecosystems/jsEvidence.js +85 -24
  39. package/lib/ecosystems/kosi.js +21 -4
  40. package/lib/ecosystems/npmutils.js +64 -0
  41. package/lib/ecosystems/parsers-dotnet.js +567 -151
  42. package/lib/ecosystems/parsers-elm.js +46 -28
  43. package/lib/ecosystems/parsers-gleam.js +3 -4
  44. package/lib/ecosystems/parsers-go.js +324 -61
  45. package/lib/ecosystems/parsers-js.js +92 -26
  46. package/lib/ecosystems/parsers-julia.js +2 -3
  47. package/lib/ecosystems/parsers-jvm.js +204 -12
  48. package/lib/ecosystems/parsers-misc.js +182 -55
  49. package/lib/ecosystems/parsers-python.js +141 -10
  50. package/lib/ecosystems/parsers-rust.js +4 -4
  51. package/lib/ecosystems/parsers-terraform.js +1573 -19
  52. package/lib/ecosystems/pythonutils.js +102 -6
  53. package/lib/ecosystems/remote/huggingface.js +3 -0
  54. package/lib/ecosystems/rubyutils.js +67 -17
  55. package/lib/ecosystems/rusi.js +22 -9
  56. package/lib/ecosystems/sbtutils.js +243 -187
  57. package/lib/ecosystems/scalacliutils.js +342 -0
  58. package/lib/ecosystems/scalasem.js +2618 -0
  59. package/lib/ecosystems/terraformRegistry.js +288 -0
  60. package/lib/ecosystems/utils.js +2 -0
  61. package/lib/ecosystems/vendoredCode.js +156 -0
  62. package/lib/evinser/evinser.js +578 -69
  63. package/lib/helpers/agenticToolutils.js +2 -2
  64. package/lib/helpers/bomSigner.js +576 -128
  65. package/lib/inventory/aiCollector.js +4 -1
  66. package/lib/inventory/analyzer.js +110 -127
  67. package/lib/inventory/atomUtils.js +171 -48
  68. package/lib/inventory/auditCategories.js +7 -0
  69. package/lib/inventory/buildToolRateLimit.js +144 -0
  70. package/lib/inventory/cacheCommands.js +2 -1
  71. package/lib/inventory/cmakeBuildDirs.js +254 -0
  72. package/lib/inventory/deps.js +501 -124
  73. package/lib/inventory/dosai.js +248 -44
  74. package/lib/inventory/dosaiParsers.js +310 -15
  75. package/lib/inventory/dynamic.js +52 -6
  76. package/lib/inventory/evidenceUtils.js +154 -109
  77. package/lib/inventory/fetchBatch.js +574 -38
  78. package/lib/inventory/fetchDiskCache.js +597 -0
  79. package/lib/inventory/forgeEnricher.js +14 -5
  80. package/lib/inventory/jvmLocalRepos.js +917 -0
  81. package/lib/inventory/mcpConfigParser.js +2 -3
  82. package/lib/inventory/parentSelection.js +146 -0
  83. package/lib/inventory/rustFormulationParser.js +3 -4
  84. package/lib/inventory/scalaCoords.js +153 -0
  85. package/lib/inventory/source.js +8 -8
  86. package/lib/inventory/spdx.js +101 -0
  87. package/lib/inventory/traceRunner.js +88 -7
  88. package/lib/inventory/tracedCrypto.js +294 -0
  89. package/lib/managers/docker.js +33 -32
  90. package/lib/managers/oci.js +24 -2
  91. package/lib/parsers/cmakeCache.js +69 -0
  92. package/lib/parsers/cmakeLists.js +16 -2
  93. package/lib/parsers/cmakePresets.js +400 -0
  94. package/lib/parsers/compileCommands.js +557 -0
  95. package/lib/parsers/strictJson.js +322 -0
  96. package/lib/parsers/terraformConfig.js +1073 -0
  97. package/lib/parsers/terraformSource.js +652 -0
  98. package/lib/server/server.js +23 -0
  99. package/lib/stages/postgen/introspection/markers.js +1 -1
  100. package/lib/stages/postgen/postgen.js +15 -1
  101. package/lib/stages/postgen/specVersionCompat.js +32 -0
  102. package/lib/validator/index.js +25 -10
  103. package/lib/validator/reporters/json.js +1 -0
  104. package/package.json +21 -21
  105. package/types/lib/audit/index.d.ts.map +1 -1
  106. package/types/lib/cli/index.d.ts.map +1 -1
  107. package/types/lib/cli/jsBom.d.ts.map +1 -1
  108. package/types/lib/cli/jvmBom.d.ts +24 -1
  109. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  110. package/types/lib/cli/managedBom.d.ts +7 -4
  111. package/types/lib/cli/managedBom.d.ts.map +1 -1
  112. package/types/lib/cli/nativeBom.d.ts +3 -1
  113. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  114. package/types/lib/core/activity.d.ts.map +1 -1
  115. package/types/lib/core/env.d.ts +9 -0
  116. package/types/lib/core/env.d.ts.map +1 -1
  117. package/types/lib/core/fs.d.ts.map +1 -1
  118. package/types/lib/core/httpClient.d.ts +13 -8
  119. package/types/lib/core/httpClient.d.ts.map +1 -1
  120. package/types/lib/core/pluginRun.d.ts +97 -0
  121. package/types/lib/core/pluginRun.d.ts.map +1 -0
  122. package/types/lib/core/processTree.d.ts +36 -0
  123. package/types/lib/core/processTree.d.ts.map +1 -0
  124. package/types/lib/core/runState.d.ts +29 -0
  125. package/types/lib/core/runState.d.ts.map +1 -0
  126. package/types/lib/core/toml.d.ts +20 -0
  127. package/types/lib/core/toml.d.ts.map +1 -0
  128. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  129. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  131. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  132. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  133. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  134. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  135. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  136. package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
  137. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  139. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  140. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  141. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  143. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  144. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  145. package/types/lib/ecosystems/gradleutils.d.ts +50 -0
  146. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  147. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  148. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  149. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  150. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  151. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  152. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  153. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  154. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  155. package/types/lib/ecosystems/parsers-go.d.ts +44 -2
  156. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  157. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  158. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  159. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  160. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  161. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  162. package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
  163. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  164. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  165. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  166. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  167. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  168. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  169. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  170. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  171. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  172. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  173. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  174. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  175. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  176. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  177. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  178. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  179. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  180. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  181. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  182. package/types/lib/ecosystems/utils.d.ts +2 -2
  183. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  184. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  185. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  186. package/types/lib/evinser/evinser.d.ts +75 -8
  187. package/types/lib/evinser/evinser.d.ts.map +1 -1
  188. package/types/lib/helpers/bomSigner.d.ts +80 -4
  189. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  190. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  191. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  192. package/types/lib/inventory/atomUtils.d.ts +82 -10
  193. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  194. package/types/lib/inventory/auditCategories.d.ts +3 -0
  195. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  196. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  197. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  198. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  199. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  200. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  201. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  202. package/types/lib/inventory/deps.d.ts +59 -1
  203. package/types/lib/inventory/deps.d.ts.map +1 -1
  204. package/types/lib/inventory/dosai.d.ts +57 -8
  205. package/types/lib/inventory/dosai.d.ts.map +1 -1
  206. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  207. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  208. package/types/lib/inventory/dynamic.d.ts +10 -0
  209. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  210. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  211. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  212. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  213. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  214. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  215. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  216. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  217. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  218. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  219. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  220. package/types/lib/inventory/parentSelection.d.ts +34 -0
  221. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  222. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  223. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  224. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  225. package/types/lib/inventory/source.d.ts.map +1 -1
  226. package/types/lib/inventory/spdx.d.ts +9 -0
  227. package/types/lib/inventory/spdx.d.ts.map +1 -1
  228. package/types/lib/inventory/traceRunner.d.ts +12 -0
  229. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  230. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  231. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  232. package/types/lib/managers/docker.d.ts.map +1 -1
  233. package/types/lib/managers/oci.d.ts +3 -1
  234. package/types/lib/managers/oci.d.ts.map +1 -1
  235. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  236. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  237. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  238. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  239. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  240. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  241. package/types/lib/parsers/compileCommands.d.ts +115 -0
  242. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  243. package/types/lib/parsers/strictJson.d.ts +46 -0
  244. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  245. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  246. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  247. package/types/lib/parsers/terraformSource.d.ts +66 -0
  248. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  249. package/types/lib/server/server.d.ts.map +1 -1
  250. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  252. package/types/lib/validator/index.d.ts +8 -0
  253. package/types/lib/validator/index.d.ts.map +1 -1
  254. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  255. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -31,6 +31,7 @@ import {
31
31
  loadNpmrcConfig,
32
32
  normalizePnpmLockKey,
33
33
  parsePnpmGitLockKey,
34
+ resolveNpmLicense,
34
35
  setNpmDevelopmentProperty,
35
36
  setNpmOptionalProperty,
36
37
  setNpmPeerProperty,
@@ -135,6 +136,11 @@ const NPM_LIFECYCLE_JS_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/i;
135
136
 
136
137
  const SHELL_ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
137
138
 
139
+ // The root component's name when neither package.json, --project-name, nor
140
+ // the directory names one (a tree rooted at a file-system root): a purl needs
141
+ // a name.
142
+ const UNNAMED_NPM_PROJECT = "unnamed-project";
143
+
138
144
  function splitLifecycleScriptCommands(scriptValue) {
139
145
  const commands = [];
140
146
  let current = "";
@@ -432,6 +438,9 @@ function analyzeNpmLifecycleScripts(pkgJsonFile, scripts) {
432
438
  * identity evidence and skip npm registry metadata fetching.
433
439
  * @param {boolean} [securityProps=false] When true, append npm security-relevant
434
440
  * properties such as lifecycle scripts, binaries, native addons, and deprecation notices.
441
+ * @param {Object} [options] Options
442
+ * @param {boolean} [options.fetchMetadata=true] When false, skip the npm
443
+ * registry round so that a caller can make one round for many manifests.
435
444
  * @returns {Promise<Array<object>>} Parsed component list, enriched with npm
436
445
  * registry metadata when available and not in simple mode.
437
446
  */
@@ -439,11 +448,15 @@ export async function parsePkgJson(
439
448
  pkgJsonFile,
440
449
  simple = false,
441
450
  securityProps = false,
451
+ options = {},
442
452
  ) {
443
453
  const pkgList = [];
444
454
  if (safeExistsSync(pkgJsonFile)) {
445
455
  try {
446
- const pkgData = JSON.parse(readFileSync(pkgJsonFile, "utf8"));
456
+ // npm and Node.js both accept a package.json that starts with a UTF-8 BOM
457
+ const pkgData = JSON.parse(
458
+ readFileSync(pkgJsonFile, "utf8").replace(/^\uFEFF/, ""),
459
+ );
447
460
  const pkgIdentifier = parsePackageJsonName(pkgData.name);
448
461
  let name = pkgIdentifier.fullName || pkgData.name;
449
462
  if (!name && !pkgJsonFile.includes("node_modules")) {
@@ -471,7 +484,7 @@ export async function parsePkgJson(
471
484
  purl: purl,
472
485
  "bom-ref": decodeURIComponent(purl),
473
486
  author: authorString,
474
- license: pkgData.license,
487
+ license: resolveNpmLicense(pkgData),
475
488
  };
476
489
  if (pkgData.homepage) {
477
490
  apkg.homepage = { url: pkgData.homepage };
@@ -662,7 +675,12 @@ export async function parsePkgJson(
662
675
  // continue regardless of error
663
676
  }
664
677
  }
665
- if (!simple && shouldFetchPackageMetadata() && pkgList?.length) {
678
+ if (
679
+ !simple &&
680
+ options.fetchMetadata !== false &&
681
+ shouldFetchPackageMetadata() &&
682
+ pkgList?.length
683
+ ) {
666
684
  if (DEBUG_MODE) {
667
685
  console.log(
668
686
  `About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgJson`,
@@ -899,6 +917,9 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
899
917
 
900
918
  let pkg;
901
919
  let purlString;
920
+ // The license the installed package.json declares, for a package whose
921
+ // lockfile entry carries none (see the spoofing check below).
922
+ let installedManifestLicense;
902
923
  const author = nodePackage.author;
903
924
  const authorString =
904
925
  author instanceof Object
@@ -908,8 +929,18 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
908
929
  : author;
909
930
  if (node === rootNode) {
910
931
  const projectGroup = options.projectGroup;
932
+ // A package.json without a name still installs, so the root of a deep
933
+ // tree can lack one: name the root component after its directory, the
934
+ // way the scans without node_modules already do, rather than fail to
935
+ // build its purl (issue 4440). A tree rooted at a file-system root has
936
+ // no directory name either, and neither has the --project-name the CLI
937
+ // derives from it.
911
938
  const projectName =
912
- "project-name" in options ? options.projectName : node.packageName;
939
+ ("project-name" in options && options.projectName) ||
940
+ node.packageName ||
941
+ basename(node.path || "") ||
942
+ node.name ||
943
+ UNNAMED_NPM_PROJECT;
913
944
  const projectVersion = options.projectVersion || node.version;
914
945
  // A package.json need not declare a version, and npm installs without
915
946
  // one, so the root of a lockfile can reach here with none. A purl carries
@@ -927,8 +958,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
927
958
  pkg = {
928
959
  author: authorString,
929
960
  group: options.projectGroup || "",
930
- name:
931
- "project-name" in options ? options.projectName : node.packageName,
961
+ name: projectName,
932
962
  version: options.projectVersion || node.version,
933
963
  type: "application",
934
964
  purl: purlString,
@@ -964,13 +994,19 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
964
994
  });
965
995
  }
966
996
  } else {
997
+ // A package.json without a name installs as a file: or workspace
998
+ // dependency too, and arborist then has no packageName for it: such a
999
+ // package goes by the name its folder has in the tree, which is also
1000
+ // the name the edges into it carry (issue 4440).
1001
+ const packageName =
1002
+ node.packageName || node.name || basename(node.path || "");
967
1003
  let namespace = "";
968
- let name = node.packageName;
969
- if (node.packageName.startsWith("@")) {
970
- const slashIndex = node.packageName.indexOf("/");
1004
+ let name = packageName;
1005
+ if (packageName.startsWith("@")) {
1006
+ const slashIndex = packageName.indexOf("/");
971
1007
  if (slashIndex > 0) {
972
- namespace = node.packageName.substring(0, slashIndex);
973
- name = node.packageName.substring(slashIndex + 1);
1008
+ namespace = packageName.substring(0, slashIndex);
1009
+ name = packageName.substring(slashIndex + 1);
974
1010
  }
975
1011
  }
976
1012
  let qualifiers = null;
@@ -1083,9 +1119,17 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1083
1119
  });
1084
1120
  }
1085
1121
  if (node?.binPaths?.length) {
1122
+ // Absolute bin paths would leak the generating machine's home
1123
+ // directory into the SBOM and differ between checkouts, so they are
1124
+ // emitted relative to the project root the package was resolved from,
1125
+ // with forward slashes on every platform (issue 4467).
1086
1126
  pkg.properties.push({
1087
1127
  name: "cdx:npm:binPaths",
1088
- value: node.binPaths.join(", "),
1128
+ value: node.binPaths
1129
+ .map((binPath) =>
1130
+ relative(srcFilePath, binPath).split(_sep).join("/"),
1131
+ )
1132
+ .join(", "),
1089
1133
  });
1090
1134
  }
1091
1135
  if (nodePackage?.bin) {
@@ -1154,9 +1198,14 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1154
1198
  try {
1155
1199
  const onDiskPkg =
1156
1200
  diskPkg || JSON.parse(readFileSync(packageJsonPath, "utf8"));
1157
- if (!onDiskPkg.name || onDiskPkg.name !== node.packageName) {
1201
+ // A package that declares no name on disk and none in the lockfile
1202
+ // (a nameless file: dependency) disagrees with nothing.
1203
+ if (
1204
+ (onDiskPkg.name || node.packageName) &&
1205
+ onDiskPkg.name !== node.packageName
1206
+ ) {
1158
1207
  console.warn(
1159
- `\x1b[1;35mWARNING: Package name spoofing detected for ${node.packageName}! Lockfile says ${node.packageName}, but disk says ${onDiskPkg.name}.\x1b[0m`,
1208
+ `\x1b[1;35mWARNING: Package name spoofing detected for ${packageName}! Lockfile says ${node.packageName}, but disk says ${onDiskPkg.name}.\x1b[0m`,
1160
1209
  );
1161
1210
  if (onDiskPkg.name) {
1162
1211
  pkg.properties.push({
@@ -1165,9 +1214,12 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1165
1214
  });
1166
1215
  }
1167
1216
  }
1168
- if (!onDiskPkg.version || onDiskPkg.version !== node.version) {
1217
+ if (
1218
+ (onDiskPkg.version || node.version) &&
1219
+ onDiskPkg.version !== node.version
1220
+ ) {
1169
1221
  console.warn(
1170
- `\x1b[1;35mWARNING: Package version spoofing detected for ${node.packageName}! Lockfile says ${node.version}, but disk says ${onDiskPkg.version}.\x1b[0m`,
1222
+ `\x1b[1;35mWARNING: Package version spoofing detected for ${packageName}! Lockfile says ${node.version}, but disk says ${onDiskPkg.version}.\x1b[0m`,
1171
1223
  );
1172
1224
  if (onDiskPkg.version) {
1173
1225
  pkg.properties.push({
@@ -1176,6 +1228,17 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1176
1228
  });
1177
1229
  }
1178
1230
  }
1231
+ // npm records only the modern `license` field in the lockfile, so a
1232
+ // package that declares the legacy `licenses` array (issue 4466) has
1233
+ // no license in the tree loaded from it. The installed manifest is
1234
+ // then the only local declaration, and only when it is the package
1235
+ // the lockfile describes.
1236
+ if (
1237
+ onDiskPkg.name === node.packageName &&
1238
+ onDiskPkg.version === node.version
1239
+ ) {
1240
+ installedManifestLicense = resolveNpmLicense(onDiskPkg);
1241
+ }
1179
1242
  } catch (_err) {
1180
1243
  // ignore
1181
1244
  }
@@ -1231,9 +1294,11 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1231
1294
  pkg.description = nodePackage.description;
1232
1295
  }
1233
1296
  }
1234
- if (nodePackage?.license) {
1297
+ const npmLicense =
1298
+ resolveNpmLicense(nodePackage) || installedManifestLicense;
1299
+ if (npmLicense) {
1235
1300
  // License will be overridden if shouldFetchLicense() is enabled
1236
- pkg.license = nodePackage.license;
1301
+ pkg.license = npmLicense;
1237
1302
  }
1238
1303
  const deprecatedMessage = nodePackage?.deprecated;
1239
1304
  if (deprecatedMessage) {
@@ -1607,7 +1672,7 @@ export async function parsePkgLock(pkgLockFile, options = {}) {
1607
1672
  `About to fetch npm registry metadata for ${pkgList.length} packages in parsePkgLock`,
1608
1673
  );
1609
1674
  }
1610
- pkgList = await getNpmMetadata(pkgList);
1675
+ pkgList = await getNpmMetadata(pkgList, undefined, { npmrcConfig });
1611
1676
  return { pkgList, dependenciesList };
1612
1677
  }
1613
1678
  return {
@@ -2457,7 +2522,7 @@ export async function parseYarnLock(
2457
2522
  `About to fetch npm registry metadata for ${pkgList.length} packages in parseYarnLock`,
2458
2523
  );
2459
2524
  }
2460
- pkgList = await getNpmMetadata(pkgList);
2525
+ pkgList = await getNpmMetadata(pkgList, undefined, { npmrcConfig });
2461
2526
  return {
2462
2527
  pkgList,
2463
2528
  dependenciesList,
@@ -2476,6 +2541,7 @@ export async function parseYarnLock(
2476
2541
  */
2477
2542
  export async function parseNodeShrinkwrap(swFile) {
2478
2543
  const pkgList = [];
2544
+ const npmrcConfig = loadNpmrcConfig(dirname(swFile));
2479
2545
  if (safeExistsSync(swFile)) {
2480
2546
  const lockData = JSON.parse(readFileSync(swFile, "utf8"));
2481
2547
  const pkgKeys = Object.keys(lockData);
@@ -2534,7 +2600,7 @@ export async function parseNodeShrinkwrap(swFile) {
2534
2600
  `About to fetch npm registry metadata for ${pkgList.length} packages in parseNodeShrinkwrap`,
2535
2601
  );
2536
2602
  }
2537
- return await getNpmMetadata(pkgList);
2603
+ return await getNpmMetadata(pkgList, undefined, { npmrcConfig });
2538
2604
  }
2539
2605
  return pkgList;
2540
2606
  }
@@ -2702,13 +2768,13 @@ export async function getVersionNumPnpm(depPkg, relativePath) {
2702
2768
  : join(version.replaceAll("../", "packages/"), "package.json");
2703
2769
  if (safeExistsSync(relativePkgJson)) {
2704
2770
  const importedComponentObj = await parsePkgJson(relativePkgJson, true);
2705
- version = importedComponentObj[0].version;
2771
+ version = importedComponentObj[0]?.version;
2706
2772
  } else if (safeExistsSync(join(version, "package.json"))) {
2707
2773
  const importedComponentObj = await parsePkgJson(
2708
2774
  join(version, "package.json"),
2709
2775
  true,
2710
2776
  );
2711
- version = importedComponentObj[0].version;
2777
+ version = importedComponentObj[0]?.version;
2712
2778
  } else if (
2713
2779
  safeExistsSync(join(version.replaceAll("../", ""), "package.json"))
2714
2780
  ) {
@@ -2716,7 +2782,7 @@ export async function getVersionNumPnpm(depPkg, relativePath) {
2716
2782
  join(version.replaceAll("../", ""), "package.json"),
2717
2783
  true,
2718
2784
  );
2719
- version = importedComponentObj[0].version;
2785
+ version = importedComponentObj[0]?.version;
2720
2786
  }
2721
2787
  } else {
2722
2788
  // version: 3.0.1(ajv@8.14.0) or, under lockfile 5, 3.0.1_ajv@8.14.0
@@ -4125,7 +4191,7 @@ export async function parsePnpmLock(
4125
4191
  `About to fetch npm registry metadata for ${pkgList.length} packages in parsePnpmLock`,
4126
4192
  );
4127
4193
  }
4128
- pkgList = await getNpmMetadata(pkgList);
4194
+ pkgList = await getNpmMetadata(pkgList, undefined, { npmrcConfig });
4129
4195
  return {
4130
4196
  pkgList,
4131
4197
  dependenciesList,
@@ -4155,7 +4221,7 @@ export async function parseBowerJson(bowerJsonFile) {
4155
4221
  group: pkgIdentifier.scope || "",
4156
4222
  version: pkgData.version || "",
4157
4223
  description: pkgData.description || "",
4158
- license: pkgData.license || "",
4224
+ license: resolveNpmLicense(pkgData) || "",
4159
4225
  properties: [
4160
4226
  {
4161
4227
  name: "internal:SrcFile",
@@ -1,7 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
- import * as toml from "smol-toml";
4
-
3
+ import { parseToml } from "../core/toml.js";
5
4
  import { tryBuildPurl } from "../inventory/purl.js";
6
5
 
7
6
  /**
@@ -260,7 +259,7 @@ const JULIA_STDLIBS = new Set([
260
259
  */
261
260
  function readTomlFile(filePath) {
262
261
  try {
263
- return toml.parse(readFileSync(filePath, "utf-8"));
262
+ return parseToml(readFileSync(filePath, "utf-8"));
264
263
  } catch (error) {
265
264
  console.warn(`Failed to parse ${filePath}: ${error.message}`);
266
265
  return undefined;
@@ -1,14 +1,23 @@
1
- import { readFileSync } from "node:fs";
2
- import { basename, dirname, resolve } from "node:path";
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import {
3
+ basename,
4
+ dirname,
5
+ isAbsolute,
6
+ join,
7
+ relative,
8
+ resolve,
9
+ } from "node:path";
3
10
 
4
11
  import { build } from "@cdxgen/cdx-purl";
5
12
  import { parseEDNString } from "edn-data";
6
13
 
7
- import { readEnvironmentVariable } from "../core/activity.js";
14
+ import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
8
15
  import { recordDegradation } from "../core/buildLedger.js";
9
16
  import { includeMavenTestScope } from "../core/env.js";
17
+ import { safeExistsSync } from "../core/fs.js";
10
18
  import { thoughtLog } from "../core/logger.js";
11
19
  import { applyPurl } from "../inventory/purl.js";
20
+ import { parseScalaArtifact } from "../inventory/scalaCoords.js";
12
21
  import { xml2js } from "../parsers/xml.js";
13
22
 
14
23
  /**
@@ -173,6 +182,158 @@ export function parsePom(pomFile) {
173
182
  return { isQuarkus, pomPurl, modules, properties, dependencies: deps };
174
183
  }
175
184
 
185
+ /** Bound on the reactor walk. A pom's module list is untrusted input, so the
186
+ * walk cannot trust it to stay finite. */
187
+ const REACTOR_MODULE_LIMIT = 500;
188
+
189
+ /**
190
+ * Every pom file of the reactor an aggregator pom builds: its own modules,
191
+ * recursively through nested aggregators. A recursive dependency:tree run
192
+ * from the aggregator resolves all of them, so their own runs repeat it.
193
+ *
194
+ * A module entry names a directory relative to its pom, sometimes a direct
195
+ * path to a pom file. Entries that are absolute, or that climb out of the
196
+ * aggregator's directory, are ignored: the scan never listed them, and the
197
+ * walk must not follow a pom's pointers outside its own tree.
198
+ *
199
+ * @param {string} rootPom Path of the aggregator pom.
200
+ * @returns {Set<string>} Pom files of the reactor, excluding the aggregator.
201
+ */
202
+ export function resolveReactorPomFiles(rootPom) {
203
+ const pomFiles = new Set();
204
+ const visited = new Set();
205
+ const rootDir = dirname(resolve(rootPom));
206
+ const queue = [resolve(rootPom)];
207
+ while (queue.length > 0 && pomFiles.size < REACTOR_MODULE_LIMIT) {
208
+ const pom = queue.shift();
209
+ if (visited.has(pom)) {
210
+ continue;
211
+ }
212
+ visited.add(pom);
213
+ let modules;
214
+ try {
215
+ modules = parsePom(pom)?.modules;
216
+ } catch (_err) {
217
+ // An unreadable module pom ends this branch of the walk.
218
+ continue;
219
+ }
220
+ for (const module of modules || []) {
221
+ if (typeof module !== "string") {
222
+ continue;
223
+ }
224
+ const cleaned = module.trim().replaceAll("\\", "/");
225
+ if (
226
+ !cleaned ||
227
+ cleaned.includes("\0") ||
228
+ cleaned.startsWith("/") ||
229
+ /^[A-Za-z]:/.test(cleaned)
230
+ ) {
231
+ continue;
232
+ }
233
+ const modulePom = resolve(
234
+ dirname(pom),
235
+ cleaned.endsWith(".xml") ? cleaned : join(cleaned, "pom.xml"),
236
+ );
237
+ const withinRoot = relative(rootDir, modulePom);
238
+ if (
239
+ !withinRoot ||
240
+ withinRoot.startsWith("..") ||
241
+ isAbsolute(withinRoot) ||
242
+ !safeExistsSync(modulePom)
243
+ ) {
244
+ continue;
245
+ }
246
+ if (visited.has(modulePom)) {
247
+ continue;
248
+ }
249
+ pomFiles.add(modulePom);
250
+ queue.push(modulePom);
251
+ }
252
+ }
253
+ return pomFiles;
254
+ }
255
+
256
+ /**
257
+ * File name stem for the trees of one recursive dependency:tree run. Maven
258
+ * expands the project expressions for each reactor module, so every module
259
+ * writes its own file; a fixed name would hold only the last module's tree.
260
+ */
261
+ export const MAVEN_MODULE_TREE_STEM =
262
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Maven expands these project expressions itself
263
+ "${project.groupId}-${project.artifactId}";
264
+
265
+ /**
266
+ * The groupId-artifactId key a module's tree file is named by.
267
+ *
268
+ * @param {string} pomFile Path of the module pom.
269
+ * @returns {string|undefined} The key, or undefined when the pom cannot be read.
270
+ */
271
+ function moduleTreeKey(pomFile) {
272
+ try {
273
+ const { groupId, artifactId } = parsePom(pomFile)?.properties || {};
274
+ return groupId && artifactId ? `${groupId}-${artifactId}` : undefined;
275
+ } catch (_err) {
276
+ return undefined;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Read the trees one dependency:tree run wrote, one file per reactor module
282
+ * (see MAVEN_MODULE_TREE_STEM), and tell which of the reactor's pom files
283
+ * they cover. The pom the run started from comes first, then its modules in
284
+ * reactor order. A module pom counts as covered only when its own tree is
285
+ * among the files, so a module the run left out still runs on its own.
286
+ *
287
+ * @param {string} treeDir Directory the run wrote its trees into.
288
+ * @param {string} rootPom The pom file the run started from.
289
+ * @returns {{trees: Array<{pomFile: string, text: string}>, covered: Set<string>}}
290
+ */
291
+ export function readMavenModuleTrees(treeDir, rootPom) {
292
+ const trees = [];
293
+ const covered = new Set();
294
+ let names;
295
+ try {
296
+ names = readdirSync(treeDir).sort();
297
+ } catch (_err) {
298
+ return { trees, covered };
299
+ }
300
+ const byKey = new Map();
301
+ for (const name of names) {
302
+ const stem = name.replace(/\.(json|txt)$/, "");
303
+ if (stem !== name && !byKey.has(stem)) {
304
+ byKey.set(stem, name);
305
+ }
306
+ }
307
+ const take = (key, pomFile) => {
308
+ const name = byKey.get(key);
309
+ if (!name) {
310
+ return false;
311
+ }
312
+ byKey.delete(key);
313
+ trees.push({
314
+ pomFile,
315
+ text: readFileSync(join(treeDir, name), { encoding: "utf-8" }),
316
+ });
317
+ return true;
318
+ };
319
+ const rootKey = moduleTreeKey(rootPom);
320
+ if (rootKey) {
321
+ take(rootKey, rootPom);
322
+ }
323
+ for (const modulePom of resolveReactorPomFiles(rootPom)) {
324
+ const key = moduleTreeKey(modulePom);
325
+ if (key && take(key, modulePom)) {
326
+ covered.add(modulePom);
327
+ }
328
+ }
329
+ // Trees no pom of the reactor names, such as the single file of a Maven
330
+ // that did not expand the expressions, are kept under the root pom.
331
+ for (const key of [...byKey.keys()]) {
332
+ take(key, rootPom);
333
+ }
334
+ return { trees, covered };
335
+ }
336
+
176
337
  function mapMavenScope(componentScope, isOptional = false) {
177
338
  if (isOptional || componentScope === "test") {
178
339
  return "optional";
@@ -471,10 +632,16 @@ export function parseMavenTree(rawOutput, pomFile) {
471
632
  /**
472
633
  * Parse mill dependencies from file
473
634
  *
635
+ * Scala artifacts are emitted in the sbt purl form: the Scala binary suffix
636
+ * is stripped from the artifact name and the version it carried is recorded
637
+ * in the cdx:scala:compilerVersion property, so one library keeps a single
638
+ * purl whichever build tool reported it.
639
+ *
474
640
  * @param {string} module name of the module
475
641
  * @param {map} dependencies the parsed dependencies
476
642
  * @param {map} relations a map containing all relations
477
643
  * @param {string} millRootPath root of the project
644
+ * @param {string} logFileName name of the tree log the module wrote
478
645
  *
479
646
  * @returns the bom-ref of the module
480
647
  */
@@ -483,6 +650,7 @@ export function parseMillDependency(
483
650
  dependencies,
484
651
  relations,
485
652
  millRootPath,
653
+ logFileName = "ivyDepsTree.log",
486
654
  ) {
487
655
  const treeRegex = /^(?<treeIndentation>(?:[?├│└─ ]{3})+)(?<dependency>.*)/m;
488
656
  const ESC = "\\x1B";
@@ -522,12 +690,7 @@ export function parseMillDependency(
522
690
  // Now put the versions we removed above back into the path and replace the brackets at the same time
523
691
  moduleFilePath = moduleFilePath.replace("[]", `/${versionNumber}/`);
524
692
  }
525
- moduleFilePath = resolve(
526
- millRootPath,
527
- "out",
528
- moduleFilePath,
529
- "ivyDepsTree.log",
530
- );
693
+ moduleFilePath = resolve(millRootPath, "out", moduleFilePath, logFileName);
531
694
  moduleComponent.properties = [
532
695
  {
533
696
  name: "internal:SrcFile",
@@ -547,7 +710,27 @@ export function parseMillDependency(
547
710
  ],
548
711
  },
549
712
  };
550
- const dependencyTreeLog = readFileSync(moduleFilePath, "utf-8");
713
+ let dependencyTreeLog;
714
+ try {
715
+ dependencyTreeLog = readFileSync(moduleFilePath, "utf-8");
716
+ } catch (_err) {
717
+ // A cross-published module writes its log under the dotted version
718
+ // directory (0.23.38) while the resolve output prints it with
719
+ // underscores (0_23_38)
720
+ try {
721
+ dependencyTreeLog = readFileSync(
722
+ moduleFilePath.replace(/(\d+)_(\d+)_(\d+)/, "$1.$2.$3"),
723
+ "utf-8",
724
+ );
725
+ } catch (_err2) {
726
+ if (DEBUG_MODE) {
727
+ console.log(
728
+ `No Mill dependency tree log was written for the module ${module}.`,
729
+ );
730
+ }
731
+ return moduleComponent["bom-ref"];
732
+ }
733
+ }
551
734
  const dependencyTreeLines = dependencyTreeLog
552
735
  .trim()
553
736
  .split("\n")
@@ -568,11 +751,20 @@ export function parseMillDependency(
568
751
  [group, name, version] = match.groups.dependency.split(":");
569
752
  version = versionRegex.exec(version).groups.version;
570
753
  }
754
+ const scalaInfo = parseScalaArtifact(name);
571
755
  const component = completeComponent({
572
756
  group,
573
- name,
757
+ name: scalaInfo.purlName,
574
758
  version,
575
759
  });
760
+ if (scalaInfo.binaryVersion) {
761
+ component.properties = [
762
+ {
763
+ name: "cdx:scala:compilerVersion",
764
+ value: scalaInfo.binaryVersion,
765
+ },
766
+ ];
767
+ }
576
768
  if (!dependencies.has(component["bom-ref"])) {
577
769
  dependencies.set(component["bom-ref"], component);
578
770
  relations.set(component["bom-ref"], []);
@@ -587,7 +779,7 @@ export function parseMillDependency(
587
779
  return moduleComponent["bom-ref"];
588
780
  }
589
781
 
590
- function completeComponent(component) {
782
+ export function completeComponent(component) {
591
783
  component["type"] = component.group ? "library" : "application";
592
784
  try {
593
785
  const purl = build({