@cdxgen/cdxgen 13.2.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 (311) hide show
  1. package/README.md +33 -14
  2. package/bin/cdxgen.js +310 -138
  3. package/bin/evinse.js +22 -0
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +119 -15
  6. package/bin/tracebom.js +69 -5
  7. package/bin/validate.js +107 -19
  8. package/bin/verify.js +172 -63
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +85 -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/bomAssembly.js +105 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +156 -58
  17. package/lib/cli/jsBom.js +99 -53
  18. package/lib/cli/jvmBom.js +708 -147
  19. package/lib/cli/managedBom.js +415 -224
  20. package/lib/cli/nativeBom.js +264 -44
  21. package/lib/core/activity.js +12 -0
  22. package/lib/core/env.js +22 -15
  23. package/lib/core/fs.js +176 -7
  24. package/lib/core/httpClient.js +159 -24
  25. package/lib/core/metadataProperties.js +97 -0
  26. package/lib/core/pluginRun.js +184 -0
  27. package/lib/core/processTree.js +100 -0
  28. package/lib/core/projectConfig.js +31 -4
  29. package/lib/core/runState.js +39 -0
  30. package/lib/core/toml.js +72 -0
  31. package/lib/ecosystems/asarutils.js +4 -2
  32. package/lib/ecosystems/bunutils.js +274 -108
  33. package/lib/ecosystems/cargoCache.js +231 -8
  34. package/lib/ecosystems/caxa.js +434 -2
  35. package/lib/ecosystems/cmakeResolver.js +76 -21
  36. package/lib/ecosystems/cppBuildContext.js +631 -0
  37. package/lib/ecosystems/cppEvidence.js +399 -51
  38. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  39. package/lib/ecosystems/denoutils.js +93 -66
  40. package/lib/ecosystems/ecosystems.js +1983 -793
  41. package/lib/ecosystems/golem.js +28 -12
  42. package/lib/ecosystems/gradleutils.js +356 -37
  43. package/lib/ecosystems/jsEvidence.js +104 -44
  44. package/lib/ecosystems/kosi.js +31 -7
  45. package/lib/ecosystems/npmutils.js +174 -9
  46. package/lib/ecosystems/parsers-dotnet.js +616 -159
  47. package/lib/ecosystems/parsers-elm.js +46 -28
  48. package/lib/ecosystems/parsers-gleam.js +3 -4
  49. package/lib/ecosystems/parsers-go.js +371 -70
  50. package/lib/ecosystems/parsers-js.js +263 -76
  51. package/lib/ecosystems/parsers-julia.js +2 -3
  52. package/lib/ecosystems/parsers-jvm.js +204 -12
  53. package/lib/ecosystems/parsers-misc.js +929 -216
  54. package/lib/ecosystems/parsers-python.js +167 -24
  55. package/lib/ecosystems/parsers-rust.js +28 -22
  56. package/lib/ecosystems/parsers-terraform.js +1573 -19
  57. package/lib/ecosystems/pythonutils.js +102 -6
  58. package/lib/ecosystems/remote/huggingface.js +3 -0
  59. package/lib/ecosystems/rubyutils.js +1492 -182
  60. package/lib/ecosystems/rusi.js +54 -10
  61. package/lib/ecosystems/sbtutils.js +243 -187
  62. package/lib/ecosystems/scalacliutils.js +342 -0
  63. package/lib/ecosystems/scalasem.js +2618 -0
  64. package/lib/ecosystems/terraformRegistry.js +288 -0
  65. package/lib/ecosystems/utils.js +2 -0
  66. package/lib/ecosystems/vendoredCode.js +156 -0
  67. package/lib/evinser/evinser.js +1008 -191
  68. package/lib/evinser/swiftsem.js +1914 -302
  69. package/lib/helpers/agenticToolutils.js +2 -2
  70. package/lib/helpers/bomSigner.js +1101 -222
  71. package/lib/inventory/aiCollector.js +4 -1
  72. package/lib/inventory/analyzer.js +110 -127
  73. package/lib/inventory/atomUtils.js +703 -43
  74. package/lib/inventory/auditCategories.js +7 -0
  75. package/lib/inventory/buildToolRateLimit.js +144 -0
  76. package/lib/inventory/cacheCommands.js +2 -1
  77. package/lib/inventory/cdxrs.js +103 -12
  78. package/lib/inventory/cmakeBuildDirs.js +254 -0
  79. package/lib/inventory/deps.js +501 -124
  80. package/lib/inventory/depsUtils.js +183 -11
  81. package/lib/inventory/display.js +4 -5
  82. package/lib/inventory/dosai.js +798 -57
  83. package/lib/inventory/dosaiParsers.js +310 -15
  84. package/lib/inventory/dynamic.js +90 -6
  85. package/lib/inventory/evidenceUtils.js +166 -113
  86. package/lib/inventory/fetchBatch.js +690 -55
  87. package/lib/inventory/fetchDiskCache.js +597 -0
  88. package/lib/inventory/forgeEnricher.js +14 -5
  89. package/lib/inventory/jvmLocalRepos.js +917 -0
  90. package/lib/inventory/mcpConfigParser.js +2 -3
  91. package/lib/inventory/parentSelection.js +146 -0
  92. package/lib/inventory/protobom.js +8 -1
  93. package/lib/inventory/purl.js +152 -3
  94. package/lib/inventory/rustFormulationParser.js +3 -4
  95. package/lib/inventory/sbomDocument.js +2 -70
  96. package/lib/inventory/scalaCoords.js +153 -0
  97. package/lib/inventory/source.js +45 -27
  98. package/lib/inventory/spdx.js +101 -0
  99. package/lib/inventory/traceRunner.js +295 -25
  100. package/lib/inventory/tracedCrypto.js +294 -0
  101. package/lib/managers/binary.js +115 -24
  102. package/lib/managers/docker.js +467 -46
  103. package/lib/managers/oci.js +63 -3
  104. package/lib/parsers/cmakeCache.js +69 -0
  105. package/lib/parsers/cmakeLists.js +466 -0
  106. package/lib/parsers/cmakePresets.js +400 -0
  107. package/lib/parsers/compileCommands.js +557 -0
  108. package/lib/parsers/largeJson.js +454 -0
  109. package/lib/parsers/npmrc.js +19 -1
  110. package/lib/parsers/strictJson.js +322 -0
  111. package/lib/parsers/terraformConfig.js +1073 -0
  112. package/lib/parsers/terraformSource.js +652 -0
  113. package/lib/server/server.js +23 -0
  114. package/lib/stages/postgen/introspection/markers.js +1 -1
  115. package/lib/stages/postgen/postgen.js +83 -16
  116. package/lib/stages/postgen/specVersionCompat.js +32 -0
  117. package/lib/validator/bomValidator.js +6 -16
  118. package/lib/validator/index.js +64 -11
  119. package/lib/validator/reporters/console.js +3 -0
  120. package/lib/validator/reporters/json.js +2 -0
  121. package/lib/validator/reporters/sarif.js +7 -0
  122. package/package.json +23 -23
  123. package/types/lib/audit/index.d.ts.map +1 -1
  124. package/types/lib/cli/bomAssembly.d.ts +2 -1
  125. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  126. package/types/lib/cli/cliOptions.d.ts +33 -1
  127. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  128. package/types/lib/cli/index.d.ts.map +1 -1
  129. package/types/lib/cli/jsBom.d.ts.map +1 -1
  130. package/types/lib/cli/jvmBom.d.ts +24 -1
  131. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  132. package/types/lib/cli/managedBom.d.ts +7 -4
  133. package/types/lib/cli/managedBom.d.ts.map +1 -1
  134. package/types/lib/cli/nativeBom.d.ts +3 -1
  135. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  136. package/types/lib/core/activity.d.ts.map +1 -1
  137. package/types/lib/core/env.d.ts +9 -0
  138. package/types/lib/core/env.d.ts.map +1 -1
  139. package/types/lib/core/fs.d.ts +55 -0
  140. package/types/lib/core/fs.d.ts.map +1 -1
  141. package/types/lib/core/httpClient.d.ts +13 -8
  142. package/types/lib/core/httpClient.d.ts.map +1 -1
  143. package/types/lib/core/metadataProperties.d.ts +39 -0
  144. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  145. package/types/lib/core/pluginRun.d.ts +97 -0
  146. package/types/lib/core/pluginRun.d.ts.map +1 -0
  147. package/types/lib/core/processTree.d.ts +36 -0
  148. package/types/lib/core/processTree.d.ts.map +1 -0
  149. package/types/lib/core/projectConfig.d.ts +6 -2
  150. package/types/lib/core/projectConfig.d.ts.map +1 -1
  151. package/types/lib/core/runState.d.ts +29 -0
  152. package/types/lib/core/runState.d.ts.map +1 -0
  153. package/types/lib/core/toml.d.ts +20 -0
  154. package/types/lib/core/toml.d.ts.map +1 -0
  155. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  156. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  157. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  159. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  160. package/types/lib/ecosystems/caxa.d.ts +14 -0
  161. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  162. package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
  163. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  164. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  165. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  166. package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
  167. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  168. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  169. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  170. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  171. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  172. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  173. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  174. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  175. package/types/lib/ecosystems/gradleutils.d.ts +70 -4
  176. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  177. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  178. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  179. package/types/lib/ecosystems/kosi.d.ts +13 -0
  180. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  181. package/types/lib/ecosystems/npmutils.d.ts +57 -0
  182. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  183. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  184. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-go.d.ts +54 -2
  188. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  189. package/types/lib/ecosystems/parsers-js.d.ts +16 -5
  190. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  191. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  199. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  200. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  201. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rubyutils.d.ts +164 -3
  205. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/rusi.d.ts +13 -0
  207. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  208. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  209. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  210. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  211. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  212. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  213. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  214. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  215. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  216. package/types/lib/ecosystems/utils.d.ts +2 -2
  217. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  218. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  219. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  220. package/types/lib/evinser/evinser.d.ts +132 -15
  221. package/types/lib/evinser/evinser.d.ts.map +1 -1
  222. package/types/lib/evinser/swiftsem.d.ts +280 -15
  223. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  224. package/types/lib/helpers/bomSigner.d.ts +182 -13
  225. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  226. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  227. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  228. package/types/lib/inventory/atomUtils.d.ts +219 -5
  229. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  230. package/types/lib/inventory/auditCategories.d.ts +3 -0
  231. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  232. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  233. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  234. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  235. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  236. package/types/lib/inventory/cdxrs.d.ts +8 -3
  237. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  238. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  239. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  240. package/types/lib/inventory/deps.d.ts +59 -1
  241. package/types/lib/inventory/deps.d.ts.map +1 -1
  242. package/types/lib/inventory/depsUtils.d.ts +15 -0
  243. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  244. package/types/lib/inventory/display.d.ts.map +1 -1
  245. package/types/lib/inventory/dosai.d.ts +121 -9
  246. package/types/lib/inventory/dosai.d.ts.map +1 -1
  247. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  248. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  249. package/types/lib/inventory/dynamic.d.ts +10 -0
  250. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  251. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  252. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  253. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  254. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  255. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  256. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  257. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  258. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  259. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  260. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/parentSelection.d.ts +34 -0
  262. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  263. package/types/lib/inventory/plugins.d.ts.map +1 -1
  264. package/types/lib/inventory/protobom.d.ts.map +1 -1
  265. package/types/lib/inventory/purl.d.ts +71 -2
  266. package/types/lib/inventory/purl.d.ts.map +1 -1
  267. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  268. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  269. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  270. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  271. package/types/lib/inventory/source.d.ts.map +1 -1
  272. package/types/lib/inventory/spdx.d.ts +9 -0
  273. package/types/lib/inventory/spdx.d.ts.map +1 -1
  274. package/types/lib/inventory/traceRunner.d.ts +59 -1
  275. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  276. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  277. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  278. package/types/lib/managers/binary.d.ts +30 -1
  279. package/types/lib/managers/binary.d.ts.map +1 -1
  280. package/types/lib/managers/docker.d.ts +29 -0
  281. package/types/lib/managers/docker.d.ts.map +1 -1
  282. package/types/lib/managers/oci.d.ts +17 -1
  283. package/types/lib/managers/oci.d.ts.map +1 -1
  284. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  285. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  286. package/types/lib/parsers/cmakeLists.d.ts +77 -0
  287. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  288. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  289. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  290. package/types/lib/parsers/compileCommands.d.ts +115 -0
  291. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  292. package/types/lib/parsers/largeJson.d.ts +52 -0
  293. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  294. package/types/lib/parsers/npmrc.d.ts +5 -0
  295. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  296. package/types/lib/parsers/strictJson.d.ts +46 -0
  297. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  298. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  299. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  300. package/types/lib/parsers/terraformSource.d.ts +66 -0
  301. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  302. package/types/lib/server/server.d.ts.map +1 -1
  303. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  304. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  306. package/types/lib/validator/index.d.ts +18 -1
  307. package/types/lib/validator/index.d.ts.map +1 -1
  308. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  309. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  310. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
  311. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -6,11 +6,11 @@
6
6
  // import parsers-js directly instead of having parsePkgJson threaded in as an
7
7
  // injected helper from the CLI.
8
8
 
9
- import { join, resolve } from "node:path";
9
+ import { dirname, isAbsolute, join, resolve } from "node:path";
10
10
 
11
- import { DEBUG_MODE } from "../core/activity.js";
12
11
  import { safeExistsSync } from "../core/fs.js";
13
12
  import { createOccurrenceEvidence } from "../inventory/evidenceUtils.js";
13
+ import { isPkgTypeOnlyImport, setNpmTypeOnlyProperty } from "./npmutils.js";
14
14
  import { parsePkgJson } from "./parsers-js.js";
15
15
 
16
16
  const NPM_BIN_IMPORT_PREFIX = "cdx:npm:bin/";
@@ -104,13 +104,95 @@ function applyNpmBinCommandEvidence(pkg, allImports) {
104
104
  }
105
105
  }
106
106
 
107
+ /**
108
+ * Full npm package name (`@scope/name` or `name`) of a component or manifest.
109
+ *
110
+ * @param {{group?: string, name?: string}} pkg Component or parsed manifest.
111
+ * @returns {string} The package name npm installs it under.
112
+ */
113
+ function npmPackageName({ group, name }) {
114
+ const scope = group?.startsWith("@") ? group.substring(1) : group;
115
+ return scope ? `@${scope}/${name}` : name;
116
+ }
117
+
118
+ /**
119
+ * Fill in a component's missing npm metadata (description, author, license,
120
+ * homepage, repository) from the package.json of its installed copy.
121
+ *
122
+ * `internal:LocalNodeModulesPath` holds either an absolute install directory
123
+ * (pnpm) or an arborist `node.location`. A location that starts with
124
+ * `node_modules/` is relative to the directory of the component's
125
+ * `internal:SrcFile`. Other relative locations, such as a workspace member or a
126
+ * package nested under one, have no recorded base and are skipped; the deep npm
127
+ * parse reads those manifests from the arborist node itself. The manifest is
128
+ * used only when it names the component's package and version, and it fills
129
+ * only the fields the component lacks.
130
+ *
131
+ * @param {object} pkg Package component to enrich in place.
132
+ */
133
+ async function fillMissingMetadataFromLocalNodeModules(pkg) {
134
+ if (pkg.description || !Array.isArray(pkg.properties)) {
135
+ return;
136
+ }
137
+ const propValue = (propName) =>
138
+ pkg.properties.find((property) => property.name === propName)?.value;
139
+ const localNodeModulesPath = propValue("internal:LocalNodeModulesPath");
140
+ if (!localNodeModulesPath) {
141
+ return;
142
+ }
143
+ let pkgJsonFile;
144
+ if (isAbsolute(localNodeModulesPath)) {
145
+ pkgJsonFile = join(localNodeModulesPath, "package.json");
146
+ } else {
147
+ const srcFile = propValue("internal:SrcFile");
148
+ if (!srcFile || !localNodeModulesPath.startsWith("node_modules/")) {
149
+ return;
150
+ }
151
+ pkgJsonFile = resolve(
152
+ dirname(srcFile),
153
+ localNodeModulesPath,
154
+ "package.json",
155
+ );
156
+ }
157
+ if (!safeExistsSync(pkgJsonFile)) {
158
+ return;
159
+ }
160
+ const lnmPkgList = await parsePkgJson(pkgJsonFile, true);
161
+ const lnmMetadata = lnmPkgList?.length === 1 ? lnmPkgList[0] : undefined;
162
+ if (
163
+ !lnmMetadata ||
164
+ npmPackageName(lnmMetadata) !== npmPackageName(pkg) ||
165
+ lnmMetadata.version !== pkg.version
166
+ ) {
167
+ return;
168
+ }
169
+ if (!pkg.description && lnmMetadata.description) {
170
+ pkg.description = lnmMetadata.description;
171
+ }
172
+ if (!pkg.author && lnmMetadata.author) {
173
+ pkg.author = lnmMetadata.author;
174
+ }
175
+ if (!pkg.license && lnmMetadata.license) {
176
+ pkg.license = lnmMetadata.license;
177
+ }
178
+ if (!pkg.homepage && lnmMetadata.homepage) {
179
+ pkg.homepage = lnmMetadata.homepage;
180
+ }
181
+ if (!pkg.repository && lnmMetadata.repository) {
182
+ pkg.repository = lnmMetadata.repository;
183
+ }
184
+ }
185
+
107
186
  /**
108
187
  * Attach JS/TS import and export usage evidence to matching package components.
109
188
  *
110
189
  * For each component, resolves its module aliases (including the deno jsr
111
190
  * specifier), records imported/exported modules as properties, attaches
112
- * occurrence evidence, and adjusts the component scope to "required" when the
113
- * package is used or "optional" when it is not.
191
+ * occurrence evidence, and promotes the component scope to "required" when the
192
+ * package is used. A package with no observed usage keeps the scope its
193
+ * manifest parser assigned: static import analysis cannot see packages loaded
194
+ * from configuration, bundler entrypoints, stylesheets, or plugins, so a
195
+ * missing import is not evidence that a package is optional.
114
196
  *
115
197
  * @param {Array<object>} pkgList Package components to enrich.
116
198
  * @param {object} allImports Map of import specifier to usage evidence objects.
@@ -128,15 +210,16 @@ export async function addEvidenceForImports(
128
210
  const impPkgs = Object.keys(allImports);
129
211
  const exportedPkgs = Object.keys(allExports);
130
212
  for (const pkg of pkgList) {
131
- if (impPkgs?.length) {
132
- // Assume that all packages are optional until we see an evidence
133
- pkg.scope = "optional";
134
- }
135
213
  const { group, name } = pkg;
136
214
  // Evidence belonging to a type must be associated with the package
137
215
  if (group === "@types") {
138
216
  continue;
139
217
  }
218
+ // Runs ahead of the alias loop, which a matching import ends with a break,
219
+ // so that every component is enriched whichever alias the source imports.
220
+ if (deep) {
221
+ await fillMissingMetadataFromLocalNodeModules(pkg);
222
+ }
140
223
  const aliases = group?.length
141
224
  ? [name, `${group}/${name}`, `@${group}/${name}`]
142
225
  : [name];
@@ -155,7 +238,6 @@ export async function addEvidenceForImports(
155
238
  aliases.push(jsrSpecifier);
156
239
  }
157
240
  }
158
- let isImported = false;
159
241
  for (const alias of aliases) {
160
242
  const isWasmAlias = /\.wasm([?#].*)?$/i.test(alias);
161
243
  const all_includes = impPkgs.filter(
@@ -213,11 +295,22 @@ export async function addEvidenceForImports(
213
295
  }
214
296
  // Identify all the imported modules of a component
215
297
  if (impPkgs.includes(alias) || all_includes.length) {
216
- isImported = true;
217
298
  let importedModules = new Set();
218
299
  let wasmExportedModules = new Set();
219
300
  const seenOccurrenceLocations = new Set();
220
- pkg.scope = "required";
301
+ // A package imported only for its types is erased at compile time.
302
+ // Scoping it excluded here, rather than when the BOM is assembled,
303
+ // keeps propagateRequiredScopeFromDependencies from promoting what it
304
+ // depends on. A package whose bin command is used is still required.
305
+ if (
306
+ isPkgTypeOnlyImport(allImports, group, name) &&
307
+ !getNpmBinCommandEvidenceKeys(allImports, pkg).length
308
+ ) {
309
+ pkg.scope = "excluded";
310
+ setNpmTypeOnlyProperty(pkg);
311
+ } else {
312
+ pkg.scope = "required";
313
+ }
221
314
  for (const subevidence of all_includes) {
222
315
  const evidences = allImports[subevidence];
223
316
  for (const evidence of evidences) {
@@ -273,39 +366,6 @@ export async function addEvidenceForImports(
273
366
  }
274
367
  break;
275
368
  }
276
- if (
277
- impPkgs?.length > 0 &&
278
- !isImported &&
279
- DEBUG_MODE &&
280
- pkg?.scope !== "optional"
281
- ) {
282
- console.debug(
283
- `\x1b[1;35mNotice: Package ${pkg.name} has no usage in code. Check if it is needed.\x1b[0m`,
284
- );
285
- }
286
- // Capture metadata such as description from local node_modules in deep mode
287
- if (deep && !pkg.description && pkg.properties) {
288
- let localNodeModulesPath;
289
- for (const aprop of pkg.properties) {
290
- if (aprop.name === "internal:LocalNodeModulesPath") {
291
- localNodeModulesPath = resolve(join(aprop.value, "package.json"));
292
- break;
293
- }
294
- }
295
- if (localNodeModulesPath && safeExistsSync(localNodeModulesPath)) {
296
- const lnmPkgList = await parsePkgJson(localNodeModulesPath, true);
297
- if (lnmPkgList && lnmPkgList.length === 1) {
298
- const lnmMetadata = lnmPkgList[0];
299
- if (lnmMetadata && Object.keys(lnmMetadata).length) {
300
- pkg.description = lnmMetadata.description;
301
- pkg.author = lnmMetadata.author;
302
- pkg.license = lnmMetadata.license;
303
- pkg.homepage = lnmMetadata.homepage;
304
- pkg.repository = lnmMetadata.repository;
305
- }
306
- }
307
- }
308
- }
309
369
  } // for alias
310
370
  // Trim the properties
311
371
  applyNpmBinCommandEvidence(pkg, allImports);
@@ -10,8 +10,13 @@ import {
10
10
  safeRmSync,
11
11
  safeSpawnSync,
12
12
  } from "../core/fs.js";
13
+ import { resolvePluginSourceDir } from "../core/pluginRun.js";
13
14
  import { resolvePluginBinary } from "../inventory/plugins.js";
14
- import { tryBuildPurl, tryParsePurl } from "../inventory/purl.js";
15
+ import {
16
+ parsePurlLoose,
17
+ tryBuildPurl,
18
+ tryParsePurl,
19
+ } from "../inventory/purl.js";
15
20
 
16
21
  /**
17
22
  * kosi — JS bridge to the Kotlin-native evidence binary (thirdparty/kosi in
@@ -46,7 +51,10 @@ export function kosiWorkspaceComponent(workspaceModule) {
46
51
  if (!workspaceModule?.purl) {
47
52
  return undefined;
48
53
  }
49
- const version = workspaceModule.purl.split("@")[1] || "unspecified";
54
+ // The version is read through the purl parser: cutting the text after `@`
55
+ // kept its escaping (`1.0.0%2Bbuild`) and any `?type=jar` qualifiers.
56
+ const version =
57
+ parsePurlLoose(workspaceModule.purl)?.version || "unspecified";
50
58
  const cleanName = (workspaceModule.name || "").replace(/^['"]|['"]+$/g, "");
51
59
  const purl =
52
60
  tryParsePurl(workspaceModule.purl) ||
@@ -59,7 +67,7 @@ export function kosiWorkspaceComponent(workspaceModule) {
59
67
  name: cleanName || workspaceModule.name,
60
68
  version,
61
69
  purl,
62
- "bom-ref": purl,
70
+ "bom-ref": decodeURIComponent(purl),
63
71
  };
64
72
  }
65
73
 
@@ -167,14 +175,25 @@ export function analyzeKosiProject(src, options = {}) {
167
175
  );
168
176
  return undefined;
169
177
  }
178
+ // cdxgen may run with its working directory already inside the project
179
+ // while the source argument is relative to a repository root; a plain
180
+ // resolve would join the two into a directory that does not exist.
181
+ const analysisDir = resolvePluginSourceDir(src, [
182
+ "pom.xml",
183
+ "build.gradle",
184
+ "build.gradle.kts",
185
+ "settings.gradle",
186
+ "settings.gradle.kts",
187
+ "classpath.txt",
188
+ ]);
170
189
  const tempDir = safeMkdtempSync(join(getTmpDir(), "kosi-"));
171
190
  try {
172
191
  const outputFile = join(tempDir, "kosi-all.json");
173
- const allArgs = kosiArgs(kosi, src, outputFile, "all");
192
+ const allArgs = kosiArgs(kosi, analysisDir, outputFile, "all");
174
193
  if (DEBUG_MODE) {
175
194
  console.log("Executing", kosi.cmd, allArgs.join(" "));
176
195
  }
177
- const report = runKosi(kosi, src, allArgs, outputFile, options);
196
+ const report = runKosi(kosi, analysisDir, allArgs, outputFile, options);
178
197
  if (!report) {
179
198
  return undefined;
180
199
  }
@@ -184,10 +203,15 @@ export function analyzeKosiProject(src, options = {}) {
184
203
  // all-pass's. Losing it would silently drop the filtered evidence,
185
204
  // so a failure here names itself but keeps the all-pass evidence.
186
205
  const reachableFile = join(tempDir, "kosi-reachable.json");
187
- const reachableArgs = kosiArgs(kosi, src, reachableFile, "reachable");
206
+ const reachableArgs = kosiArgs(
207
+ kosi,
208
+ analysisDir,
209
+ reachableFile,
210
+ "reachable",
211
+ );
188
212
  const reachableReport = runKosi(
189
213
  kosi,
190
- src,
214
+ analysisDir,
191
215
  reachableArgs,
192
216
  reachableFile,
193
217
  options,
@@ -2,8 +2,11 @@ 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";
9
+ import { spdxLicenses } from "../inventory/spdx.js";
7
10
  import { parseNpmrc, parseNpmrcFromEnv } from "../parsers/npmrc.js";
8
11
 
9
12
  const npmPackageHydrationFields = [
@@ -17,6 +20,9 @@ const npmPackageHydrationFields = [
17
20
  "homepage",
18
21
  "keywords",
19
22
  "license",
23
+ // Legacy plural form kept by long-lived packages such as fuzzy 0.1.3: the
24
+ // manifest carries `licenses` instead of `license` (issue 4466).
25
+ "licenses",
20
26
  "repository",
21
27
  ];
22
28
 
@@ -84,6 +90,89 @@ export function setNpmPeerProperty(pkg) {
84
90
  addComponentProperty(pkg, "cdx:npm:package:peer", "true");
85
91
  }
86
92
 
93
+ /**
94
+ * Marks an npm component as imported only for its TypeScript types.
95
+ *
96
+ * @param {object} pkg Component object to annotate
97
+ * @returns {void}
98
+ */
99
+ export function setNpmTypeOnlyProperty(pkg) {
100
+ addComponentProperty(pkg, "cdx:npm:package:type-only", "true");
101
+ }
102
+
103
+ /**
104
+ * Clean an npm version the way `npm publish` does before it records a release.
105
+ *
106
+ * Publishing a directory runs the manifest through `npm pkg fix`
107
+ * (`fixVersionField` in `@npmcli/package-json`, which calls `semver.clean` in
108
+ * loose mode) and then through libnpmpublish, which calls `semver.clean` again
109
+ * in strict mode and records the result as the packument version, the
110
+ * dist-tag, the `_id` and the tarball file name. Semver build metadata
111
+ * (`1.0.0+build.5`), a leading `v` or `=`, surrounding whitespace and leading
112
+ * zeros are dropped; a prerelease is kept.
113
+ *
114
+ * The `package.json` inside the published tarball keeps the original string,
115
+ * and so do the lockfiles npm, pnpm, yarn and bun write for a project, a
116
+ * workspace member or a git, file or link dependency. The cleaned form is the
117
+ * version the npm registry knows the package by.
118
+ *
119
+ * @param {string} version Version as a manifest or lockfile records it
120
+ * @returns {string|null} The cleaned version, or null when npm would reject `version` as invalid semver
121
+ */
122
+ export function cleanNpmVersion(version) {
123
+ if (typeof version !== "string" || !version.trim()) {
124
+ return null;
125
+ }
126
+ const looseClean = clean(version, { loose: true });
127
+ if (!looseClean) {
128
+ return null;
129
+ }
130
+ return clean(looseClean);
131
+ }
132
+
133
+ /**
134
+ * Record the version npm would publish when it differs from the recorded one.
135
+ *
136
+ * Sets `cdx:npm:cleanedVersion` on an npm component whose version is valid
137
+ * semver but not in the form {@link cleanNpmVersion} produces, for example
138
+ * `1.0.0+build.5` (published as `1.0.0`). The component's `version` and `purl`
139
+ * keep the recorded string: that is what the manifest and lockfile say. A
140
+ * component whose version is already clean, or not semver at all (a git
141
+ * commit, a `file:` path), is left without the property, and a stale value from
142
+ * an earlier pass is replaced.
143
+ *
144
+ * @param {object} component Component or package object with `version` and `purl`
145
+ * @returns {object} The same component, for chaining
146
+ */
147
+ export function setNpmCleanedVersionProperty(component) {
148
+ if (!component || typeof component !== "object") {
149
+ return component;
150
+ }
151
+ const purl = component.purl;
152
+ if (typeof purl !== "string" || !purl.startsWith("pkg:npm/")) {
153
+ return component;
154
+ }
155
+ const cleaned = cleanNpmVersion(component.version);
156
+ const wanted = cleaned && cleaned !== component.version ? cleaned : undefined;
157
+ const existing = component.properties?.filter(
158
+ (property) => property?.name === "cdx:npm:cleanedVersion",
159
+ );
160
+ if (
161
+ (!existing?.length && !wanted) ||
162
+ (existing?.length === 1 && existing[0].value === wanted)
163
+ ) {
164
+ return component;
165
+ }
166
+ const properties = (component.properties || []).filter(
167
+ (property) => property?.name !== "cdx:npm:cleanedVersion",
168
+ );
169
+ if (wanted) {
170
+ properties.push({ name: "cdx:npm:cleanedVersion", value: wanted });
171
+ }
172
+ component.properties = properties;
173
+ return component;
174
+ }
175
+
87
176
  /**
88
177
  * Helper function to create a properly encoded workspace PURL
89
178
  *
@@ -130,14 +219,18 @@ export function findMatchingNpmWorkspace(workspacePackages, packageName) {
130
219
  return undefined;
131
220
  }
132
221
 
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
- );
222
+ // Compare the parsed coordinates: a prefix match on the purl text would let
223
+ // `ws` match `wsmember`, and the reference may be escaped or decoded.
224
+ return workspacePackages.find((workspacePackage) => {
225
+ const parts = parsePurlLoose(workspacePackage);
226
+ if (parts?.type !== "npm") {
227
+ return false;
228
+ }
229
+ const fullName = parts.namespace
230
+ ? `${parts.namespace}/${parts.name}`
231
+ : parts.name;
232
+ return fullName === packageName;
233
+ });
141
234
  }
142
235
 
143
236
  /**
@@ -201,6 +294,12 @@ export function collectNpmManifestSources(node) {
201
294
  return manifestSources;
202
295
  }
203
296
  for (const edge of node.edgesIn) {
297
+ // Arborist adds a `workspace` edge from the root to every member, with an
298
+ // absolute `file:` spec it builds from the checkout path. No manifest
299
+ // declares it, and copying it would put the local path into the BOM.
300
+ if (edge?.workspace || edge?.type === "workspace") {
301
+ continue;
302
+ }
204
303
  const manifestSource = classifyNpmManifestSource(edge?.spec);
205
304
  if (!manifestSource) {
206
305
  continue;
@@ -215,6 +314,66 @@ export function collectNpmManifestSources(node) {
215
314
  return manifestSources;
216
315
  }
217
316
 
317
+ /**
318
+ * Whether a value of the legacy `licenses` field names a license: a non-empty
319
+ * string, or an object with a non-empty `type`.
320
+ *
321
+ * @param {*} entry One value of the legacy field
322
+ * @returns {boolean} True when the entry names a license
323
+ */
324
+ function isLegacyNpmLicenseEntry(entry) {
325
+ if (typeof entry === "string") {
326
+ return entry.trim() !== "";
327
+ }
328
+ return typeof entry?.type === "string" && entry.type.trim() !== "";
329
+ }
330
+
331
+ /**
332
+ * Resolve the license of an npm-style manifest, honouring the legacy plural
333
+ * `licenses` field that packages published before the SPDX convention still
334
+ * carry (issue 4466). A modern `license` value is returned unchanged. The
335
+ * legacy field may hold one entry or an array of them, each a string or a
336
+ * `{ type, url }` object; one entry is returned as it is. npm documents an
337
+ * array of several entries as a choice between them, so when every entry is an
338
+ * SPDX identifier the array becomes the equivalent `MIT OR Apache-2.0`
339
+ * expression, and otherwise stays a list for `getLicenses` to convert entry by
340
+ * entry.
341
+ *
342
+ * @param {object} pkgData Parsed package.json (or bower.json) manifest
343
+ * @returns {string|object|Array|undefined} The effective license value
344
+ */
345
+ export function resolveNpmLicense(pkgData) {
346
+ if (!pkgData || typeof pkgData !== "object") {
347
+ return undefined;
348
+ }
349
+ const license = pkgData.license;
350
+ // An empty or null license is no declaration and defers to `licenses`.
351
+ if (
352
+ license !== undefined &&
353
+ license !== null &&
354
+ !(typeof license === "string" && license.trim() === "")
355
+ ) {
356
+ return license;
357
+ }
358
+ const legacyEntries = (
359
+ Array.isArray(pkgData.licenses) ? pkgData.licenses : [pkgData.licenses]
360
+ ).filter(isLegacyNpmLicenseEntry);
361
+ if (legacyEntries.length <= 1) {
362
+ return legacyEntries[0];
363
+ }
364
+ const licenseIds = [
365
+ ...new Set(
366
+ legacyEntries.map((entry) =>
367
+ (typeof entry === "string" ? entry : entry.type).trim(),
368
+ ),
369
+ ),
370
+ ];
371
+ if (licenseIds.every((licenseId) => spdxLicenses.includes(licenseId))) {
372
+ return licenseIds.join(" OR ");
373
+ }
374
+ return legacyEntries;
375
+ }
376
+
218
377
  /**
219
378
  * Hydrates sparse npm package metadata from the installed package.json in deep mode.
220
379
  * Existing metadata on the Arborist node wins over on-disk values.
@@ -520,7 +679,9 @@ export function parsePnpmGitLockKey(lockKey) {
520
679
  * @returns {Object<string, object>} Map of lookup key to git package reference.
521
680
  */
522
681
  export function buildPnpmGitPkgRefs(packages, snapshots, npmrcConfig = {}) {
523
- const gitPkgRefs = {};
682
+ // A null prototype keeps inherited Object properties (such as `constructor`)
683
+ // from masquerading as lookup hits for keys that were never registered.
684
+ const gitPkgRefs = Object.create(null);
524
685
  const registerEntry = (lockKey, packageNode) => {
525
686
  const resolution = packageNode?.resolution;
526
687
  const parsed = parsePnpmGitLockKey(lockKey);
@@ -531,6 +692,10 @@ export function buildPnpmGitPkgRefs(packages, snapshots, npmrcConfig = {}) {
531
692
  return;
532
693
  }
533
694
  const { group, name, gitSpec, fullName, packageName } = parsed;
695
+ if (name === "__proto__" || packageName === "__proto__") {
696
+ // A crafted lock key cannot become a lookup entry under this name.
697
+ return;
698
+ }
534
699
  const version = packageNode?.version || resolution?.commit || "";
535
700
  const repo = resolution?.repo || "";
536
701
  const commit = resolution?.commit || "";