@cdxgen/cdxgen 13.6.0 → 13.7.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 (264) hide show
  1. package/README.md +15 -8
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +256 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +25 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +18 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/pqc-migration.json +239 -0
  15. package/data/remediations.json +52 -0
  16. package/data/rules/cbom-security.yaml +4 -3
  17. package/data/rules/package-integrity.yaml +28 -0
  18. package/lib/audit/index.js +25 -3
  19. package/lib/cli/bomAssembly.js +80 -1
  20. package/lib/cli/cliOptions.js +121 -17
  21. package/lib/cli/index.js +609 -898
  22. package/lib/cli/jsBom.js +3 -0
  23. package/lib/cli/jvmBom.js +365 -40
  24. package/lib/cli/managedBom.js +19 -3
  25. package/lib/cli/nativeBom.js +10 -0
  26. package/lib/core/env.js +26 -0
  27. package/lib/core/fs.js +126 -3
  28. package/lib/core/globPatterns.js +218 -0
  29. package/lib/core/processTree.js +99 -12
  30. package/lib/core/projectConfig.js +332 -25
  31. package/lib/core/toolRegistry.js +15 -1
  32. package/lib/ecosystems/denoutils.js +2 -2
  33. package/lib/ecosystems/golem.js +3 -8
  34. package/lib/ecosystems/gradleutils.js +1082 -6
  35. package/lib/ecosystems/jsEvidence.js +90 -38
  36. package/lib/ecosystems/kosi.js +3 -5
  37. package/lib/ecosystems/npmVersionResolution.js +376 -0
  38. package/lib/ecosystems/parsers-python.js +89 -28
  39. package/lib/ecosystems/parsers-rust.js +117 -15
  40. package/lib/ecosystems/remote/huggingface.js +22 -0
  41. package/lib/ecosystems/rusi.js +2 -4
  42. package/lib/ecosystems/scalasem.js +2 -1
  43. package/lib/evinser/evinser.js +196 -34
  44. package/lib/evinser/scalasem.js +5 -1
  45. package/lib/helpers/agenticToolutils.js +112 -1
  46. package/lib/helpers/bomSigner.js +43 -9
  47. package/lib/helpers/jssSigner.js +1276 -0
  48. package/lib/inventory/agentBlueprint.js +193 -0
  49. package/lib/inventory/agentToolNames.js +145 -0
  50. package/lib/inventory/aiCollector.js +153 -7
  51. package/lib/inventory/aiInventory.js +44 -8
  52. package/lib/inventory/aiProviders.js +324 -0
  53. package/lib/inventory/bomUtils.js +8 -1
  54. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  55. package/lib/inventory/citations.js +31 -2
  56. package/lib/inventory/communityAiConfigParser.js +978 -80
  57. package/lib/inventory/cyclonedx20.js +152 -0
  58. package/lib/inventory/cyclonedx20Common.js +286 -0
  59. package/lib/inventory/cyclonedx20Derived.js +159 -0
  60. package/lib/inventory/cyclonedx20Parties.js +552 -0
  61. package/lib/inventory/cyclonedx20Read.js +768 -0
  62. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  63. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  64. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  65. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  66. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  67. package/lib/inventory/cyclonedx20Render.js +766 -0
  68. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  69. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  70. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  71. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  72. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  73. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  74. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  75. package/lib/inventory/cyclonedx20Tables.js +594 -0
  76. package/lib/inventory/deps.js +106 -13
  77. package/lib/inventory/dosai.js +236 -1
  78. package/lib/inventory/dosaiParsers.js +3 -6
  79. package/lib/inventory/dotnetRuntime.js +164 -14
  80. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  81. package/lib/inventory/envcontext.js +132 -38
  82. package/lib/inventory/evidenceUtils.js +2 -1
  83. package/lib/inventory/formulationParsers.js +95 -13
  84. package/lib/inventory/hbom.js +45 -6
  85. package/lib/inventory/hbomLoader.js +166 -0
  86. package/lib/inventory/mcpConfigParser.js +5 -0
  87. package/lib/inventory/occurrenceKey.js +90 -0
  88. package/lib/inventory/pqcMigration.js +426 -0
  89. package/lib/inventory/rustFormulationParser.js +21 -10
  90. package/lib/inventory/source.js +65 -6
  91. package/lib/parsers/huggingfaceManifest.js +9 -1
  92. package/lib/parsers/huggingfaceUtils.js +18 -0
  93. package/lib/parsers/kiroPermissions.js +166 -0
  94. package/lib/server/server.js +36 -34
  95. package/lib/stages/postgen/postgen.js +41 -1
  96. package/lib/stages/postgen/specVersionCompat.js +76 -0
  97. package/lib/validator/bomValidator.js +8 -1
  98. package/lib/validator/complianceEngine.js +30 -3
  99. package/lib/validator/complianceRules.js +1416 -4
  100. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  101. package/lib/validator/index.js +48 -7
  102. package/lib/validator/reporters/json.js +6 -0
  103. package/package.json +9 -9
  104. package/types/bin/repl.d.ts.map +1 -1
  105. package/types/lib/audit/index.d.ts +6 -2
  106. package/types/lib/audit/index.d.ts.map +1 -1
  107. package/types/lib/cli/bomAssembly.d.ts +16 -0
  108. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  109. package/types/lib/cli/cliOptions.d.ts +6 -0
  110. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  111. package/types/lib/cli/index.d.ts.map +1 -1
  112. package/types/lib/cli/jsBom.d.ts +2 -1
  113. package/types/lib/cli/jsBom.d.ts.map +1 -1
  114. package/types/lib/cli/jvmBom.d.ts +15 -0
  115. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  116. package/types/lib/cli/managedBom.d.ts.map +1 -1
  117. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  118. package/types/lib/core/boundedFile.d.ts +30 -0
  119. package/types/lib/core/boundedFile.d.ts.map +1 -1
  120. package/types/lib/core/env.d.ts +10 -0
  121. package/types/lib/core/env.d.ts.map +1 -1
  122. package/types/lib/core/fs.d.ts +4 -0
  123. package/types/lib/core/fs.d.ts.map +1 -1
  124. package/types/lib/core/globPatterns.d.ts +70 -0
  125. package/types/lib/core/globPatterns.d.ts.map +1 -0
  126. package/types/lib/core/paths.d.ts +9 -0
  127. package/types/lib/core/paths.d.ts.map +1 -1
  128. package/types/lib/core/processTree.d.ts +16 -5
  129. package/types/lib/core/processTree.d.ts.map +1 -1
  130. package/types/lib/core/projectConfig.d.ts +47 -8
  131. package/types/lib/core/projectConfig.d.ts.map +1 -1
  132. package/types/lib/core/toolRegistry.d.ts.map +1 -1
  133. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  134. package/types/lib/ecosystems/gradleutils.d.ts +248 -8
  135. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  136. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  137. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  139. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  140. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  141. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  142. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  143. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  144. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  145. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  146. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  147. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  148. package/types/lib/evinser/evinser.d.ts +61 -9
  149. package/types/lib/evinser/evinser.d.ts.map +1 -1
  150. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  151. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  152. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  153. package/types/lib/helpers/bomSigner.d.ts +20 -3
  154. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  155. package/types/lib/helpers/jssSigner.d.ts +288 -0
  156. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  157. package/types/lib/helpers/vsixutils.d.ts +303 -19
  158. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  159. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  160. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  161. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  162. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  163. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  164. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  165. package/types/lib/inventory/aiProviders.d.ts +85 -0
  166. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  167. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  168. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  169. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  170. package/types/lib/inventory/citations.d.ts +5 -0
  171. package/types/lib/inventory/citations.d.ts.map +1 -1
  172. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  173. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  174. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  175. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  176. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  177. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  178. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  179. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  180. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  181. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  182. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  183. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  184. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  185. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  186. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  187. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  188. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  189. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  190. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  191. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  192. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  193. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  194. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  195. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  196. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  197. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  198. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  199. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  200. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  201. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  202. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  203. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  204. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  205. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  206. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  207. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  208. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  209. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  210. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  211. package/types/lib/inventory/deps.d.ts +35 -2
  212. package/types/lib/inventory/deps.d.ts.map +1 -1
  213. package/types/lib/inventory/dosai.d.ts +19 -0
  214. package/types/lib/inventory/dosai.d.ts.map +1 -1
  215. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  216. package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
  217. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
  218. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  219. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  220. package/types/lib/inventory/envcontext.d.ts +22 -6
  221. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  222. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  224. package/types/lib/inventory/hbom.d.ts +11 -0
  225. package/types/lib/inventory/hbom.d.ts.map +1 -1
  226. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  227. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  228. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  229. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  230. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  231. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  232. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  233. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  234. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  235. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  236. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  237. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  238. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  239. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  240. package/types/lib/inventory/source.d.ts +15 -0
  241. package/types/lib/inventory/source.d.ts.map +1 -1
  242. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  243. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  244. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  245. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  246. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  247. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  248. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  249. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  250. package/types/lib/server/server.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  252. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  253. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  254. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  255. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  256. package/types/lib/validator/bomValidator.d.ts +5 -1
  257. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  258. package/types/lib/validator/complianceEngine.d.ts +4 -4
  259. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  260. package/types/lib/validator/complianceRules.d.ts +14 -2
  261. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  262. package/types/lib/validator/index.d.ts +10 -0
  263. package/types/lib/validator/index.d.ts.map +1 -1
  264. package/types/lib/validator/reporters/json.d.ts.map +1 -1
@@ -11,6 +11,11 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
11
11
  import { safeExistsSync } from "../core/fs.js";
12
12
  import { createOccurrenceEvidence } from "../inventory/evidenceUtils.js";
13
13
  import { isPkgTypeOnlyImport, setNpmTypeOnlyProperty } from "./npmutils.js";
14
+ import {
15
+ createNpmVersionChooser,
16
+ npmPackageName,
17
+ projectRootRefs,
18
+ } from "./npmVersionResolution.js";
14
19
  import { parsePkgJson } from "./parsers-js.js";
15
20
 
16
21
  const NPM_BIN_IMPORT_PREFIX = "cdx:npm:bin/";
@@ -69,10 +74,30 @@ function getNpmBinCommandEvidenceKeys(allImports, pkg) {
69
74
  );
70
75
  }
71
76
 
72
- function applyNpmBinCommandEvidence(pkg, allImports) {
77
+ /**
78
+ * Credit a package with the `cdx:npm:bin/<command>` evidence of its bin
79
+ * commands, limited to the files that resolve to this version when several are
80
+ * installed.
81
+ *
82
+ * @param {object} pkg Package component, changed in place
83
+ * @param {object} allImports Map of import specifier to usage evidence objects
84
+ * @param {object} versionChooser Chooser from `createNpmVersionChooser`
85
+ * @returns {boolean} Whether a bin command of this version is used
86
+ */
87
+ function applyNpmBinCommandEvidence(pkg, allImports, versionChooser) {
73
88
  const binEvidenceKeys = getNpmBinCommandEvidenceKeys(allImports, pkg);
74
89
  if (!binEvidenceKeys.length) {
75
- return;
90
+ return false;
91
+ }
92
+ const binEvidences = binEvidenceKeys.flatMap((key) =>
93
+ Array.from(allImports[key] || []).filter(
94
+ (evidence) =>
95
+ !versionChooser.isAmbiguous(pkg) ||
96
+ (evidence?.fileName && versionChooser.allows(pkg, evidence.fileName)),
97
+ ),
98
+ );
99
+ if (versionChooser.isAmbiguous(pkg) && !binEvidences.length) {
100
+ return false;
76
101
  }
77
102
  pkg.scope = "required";
78
103
  const seenOccurrenceLocations = new Set(
@@ -81,38 +106,26 @@ function applyNpmBinCommandEvidence(pkg, allImports) {
81
106
  `${occurrence.location}${occurrence.line ? `#${occurrence.line}` : ""}`,
82
107
  ),
83
108
  );
84
- for (const binEvidenceKey of binEvidenceKeys) {
85
- for (const evidence of allImports[binEvidenceKey] || []) {
86
- if (!evidence?.fileName) {
87
- continue;
88
- }
89
- const occurrence = createOccurrenceEvidence(evidence.fileName, {
90
- ...(evidence.lineNumber ? { line: evidence.lineNumber } : {}),
91
- });
92
- if (!occurrence) {
93
- continue;
94
- }
95
- const occurrenceLocation = `${occurrence.location}${occurrence.line ? `#${occurrence.line}` : ""}`;
96
- if (seenOccurrenceLocations.has(occurrenceLocation)) {
97
- continue;
98
- }
99
- pkg.evidence = pkg.evidence || {};
100
- pkg.evidence.occurrences = pkg.evidence.occurrences || [];
101
- pkg.evidence.occurrences.push(occurrence);
102
- seenOccurrenceLocations.add(occurrenceLocation);
109
+ for (const evidence of binEvidences) {
110
+ if (!evidence?.fileName) {
111
+ continue;
112
+ }
113
+ const occurrence = createOccurrenceEvidence(evidence.fileName, {
114
+ ...(evidence.lineNumber ? { line: evidence.lineNumber } : {}),
115
+ });
116
+ if (!occurrence) {
117
+ continue;
118
+ }
119
+ const occurrenceLocation = `${occurrence.location}${occurrence.line ? `#${occurrence.line}` : ""}`;
120
+ if (seenOccurrenceLocations.has(occurrenceLocation)) {
121
+ continue;
103
122
  }
123
+ pkg.evidence = pkg.evidence || {};
124
+ pkg.evidence.occurrences = pkg.evidence.occurrences || [];
125
+ pkg.evidence.occurrences.push(occurrence);
126
+ seenOccurrenceLocations.add(occurrenceLocation);
104
127
  }
105
- }
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;
128
+ return true;
116
129
  }
117
130
 
118
131
  /**
@@ -194,11 +207,20 @@ async function fillMissingMetadataFromLocalNodeModules(pkg) {
194
207
  * from configuration, bundler entrypoints, stylesheets, or plugins, so a
195
208
  * missing import is not evidence that a package is optional.
196
209
  *
210
+ * When the lock file holds several versions of a package, an import is
211
+ * evidence only for the version the importing file resolves to (see
212
+ * `createNpmVersionChooser`), so a copy nested under another package is not
213
+ * credited with, or promoted to required by, the project's own imports.
214
+ *
197
215
  * @param {Array<object>} pkgList Package components to enrich.
198
216
  * @param {object} allImports Map of import specifier to usage evidence objects.
199
217
  * @param {object} allExports Map of export specifier to export evidence objects.
200
218
  * @param {boolean} deep When true, fill in missing description/author/license
201
219
  * metadata from the local node_modules copy of each package.
220
+ * @param {object} [context] Project facts that pick the version a file uses.
221
+ * @param {string} [context.srcDir] Directory the evidence file names are relative to.
222
+ * @param {Array<object>} [context.dependencies] Dependency tree of the packages.
223
+ * @param {object} [context.parentComponent] Project component, with any workspace members as its components.
202
224
  * @returns {Promise<Array<object>>} The enriched package list (same reference as pkgList).
203
225
  */
204
226
  export async function addEvidenceForImports(
@@ -206,9 +228,15 @@ export async function addEvidenceForImports(
206
228
  allImports,
207
229
  allExports,
208
230
  deep,
231
+ context = {},
209
232
  ) {
210
233
  const impPkgs = Object.keys(allImports);
211
234
  const exportedPkgs = Object.keys(allExports);
235
+ const versionChooser = createNpmVersionChooser(pkgList, {
236
+ srcDir: context.srcDir,
237
+ dependencies: context.dependencies,
238
+ rootRefs: projectRootRefs(context.parentComponent),
239
+ });
212
240
  for (const pkg of pkgList) {
213
241
  const { group, name } = pkg;
214
242
  // Evidence belonging to a type must be associated with the package
@@ -238,6 +266,7 @@ export async function addEvidenceForImports(
238
266
  aliases.push(jsrSpecifier);
239
267
  }
240
268
  }
269
+ let creditedByImports = false;
241
270
  for (const alias of aliases) {
242
271
  const isWasmAlias = /\.wasm([?#].*)?$/i.test(alias);
243
272
  const all_includes = impPkgs.filter(
@@ -293,8 +322,24 @@ export async function addEvidenceForImports(
293
322
  });
294
323
  }
295
324
  }
296
- // Identify all the imported modules of a component
297
- if (impPkgs.includes(alias) || all_includes.length) {
325
+ // Identify all the imported modules of a component. Of several
326
+ // installed versions, only the one a file resolves to takes its imports.
327
+ const isAmbiguous = versionChooser.isAmbiguous(pkg);
328
+ const evidencesOf = (subevidence) => {
329
+ const evidences = Array.from(allImports[subevidence] || []);
330
+ return isAmbiguous
331
+ ? evidences.filter(
332
+ (evidence) =>
333
+ evidence?.fileName &&
334
+ versionChooser.allows(pkg, evidence.fileName),
335
+ )
336
+ : evidences;
337
+ };
338
+ const usedIncludes = isAmbiguous
339
+ ? all_includes.filter((subevidence) => evidencesOf(subevidence).length)
340
+ : all_includes;
341
+ if ((!isAmbiguous && impPkgs.includes(alias)) || usedIncludes.length) {
342
+ creditedByImports = true;
298
343
  let importedModules = new Set();
299
344
  let wasmExportedModules = new Set();
300
345
  const seenOccurrenceLocations = new Set();
@@ -311,9 +356,8 @@ export async function addEvidenceForImports(
311
356
  } else {
312
357
  pkg.scope = "required";
313
358
  }
314
- for (const subevidence of all_includes) {
315
- const evidences = allImports[subevidence];
316
- for (const evidence of evidences) {
359
+ for (const subevidence of usedIncludes) {
360
+ for (const evidence of evidencesOf(subevidence)) {
317
361
  if (evidence && Object.keys(evidence).length && evidence.fileName) {
318
362
  const occurrence = createOccurrenceEvidence(evidence.fileName, {
319
363
  ...(evidence.lineNumber ? { line: evidence.lineNumber } : {}),
@@ -367,8 +411,16 @@ export async function addEvidenceForImports(
367
411
  break;
368
412
  }
369
413
  } // for alias
414
+ if (applyNpmBinCommandEvidence(pkg, allImports, versionChooser)) {
415
+ creditedByImports = true;
416
+ }
417
+ // Every import of this package's name resolves to another installed
418
+ // version. buildBomNSData reads the flag so that matching by name alone
419
+ // does not promote this copy to required.
420
+ if (versionChooser.isAmbiguous(pkg) && !creditedByImports) {
421
+ pkg._npmImportsResolveElsewhere = true;
422
+ }
370
423
  // Trim the properties
371
- applyNpmBinCommandEvidence(pkg, allImports);
372
424
  pkg.properties = pkg.properties.filter(
373
425
  (p) => p.name !== "internal:LocalNodeModulesPath",
374
426
  );
@@ -11,6 +11,7 @@ import {
11
11
  safeSpawnSync,
12
12
  } from "../core/fs.js";
13
13
  import { resolvePluginSourceDir } from "../core/pluginRun.js";
14
+ import { occurrenceLocationKey } from "../inventory/occurrenceKey.js";
14
15
  import { resolvePluginBinary } from "../inventory/plugins.js";
15
16
  import {
16
17
  parsePurlLoose,
@@ -331,10 +332,7 @@ function positionLocation(position) {
331
332
  if (!position?.filename) {
332
333
  return undefined;
333
334
  }
334
- if (position.line && position.line > 0) {
335
- return `${position.filename}#${position.line}`;
336
- }
337
- return position.filename;
335
+ return occurrenceLocationKey(position.filename, position.line);
338
336
  }
339
337
 
340
338
  function purlWithoutVersion(purl) {
@@ -843,7 +841,7 @@ function addSecuritySignalEvidence(
843
841
  // so the filePath is the only location it has.
844
842
  const location =
845
843
  positionLocation(signal.position) ||
846
- (signal.filePath ? signal.filePath : undefined);
844
+ occurrenceLocationKey(signal.filePath);
847
845
  if (location) {
848
846
  addSetValue(purlLocationMap, purl, location);
849
847
  }
@@ -0,0 +1,376 @@
1
+ // Which installed version of an npm package a source file uses.
2
+ //
3
+ // A lock file often holds several versions of one package: the one the project
4
+ // asked for, hoisted to the top-level node_modules, and older or newer copies
5
+ // nested under the packages that need them. A file of the project loads the
6
+ // copy Node.js finds first when it walks up from the file's directory, so only
7
+ // that version is used there. Crediting the file's imports to every version
8
+ // would mark the nested copies as used by the project.
9
+
10
+ import { dirname, isAbsolute, resolve } from "node:path";
11
+
12
+ import { Purl } from "@cdxgen/cdx-purl";
13
+
14
+ import { occurrenceKeyLocation } from "../inventory/occurrenceKey.js";
15
+
16
+ /**
17
+ * Full npm package name (`@scope/name` or `name`) of a component or manifest.
18
+ *
19
+ * @param {{group?: string, name?: string}} pkg Component or parsed manifest
20
+ * @returns {string} The package name npm installs it under
21
+ */
22
+ export function npmPackageName({ group, name }) {
23
+ const scope = group?.startsWith("@") ? group.substring(1) : group;
24
+ return scope ? `@${scope}/${name}` : name;
25
+ }
26
+
27
+ /**
28
+ * Full npm package name of an npm purl.
29
+ *
30
+ * @param {string} purl Package URL
31
+ * @returns {string|undefined} `@scope/name` or `name`; undefined for a purl that is not npm or does not parse
32
+ */
33
+ export function npmPackageNameOfPurl(purl) {
34
+ try {
35
+ const purlObj = Purl.parse(purl);
36
+ if (purlObj.type !== "npm") {
37
+ return undefined;
38
+ }
39
+ return purlObj.namespace
40
+ ? `${purlObj.namespace}/${purlObj.name}`
41
+ : purlObj.name;
42
+ } catch (_e) {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Directory a package-lock component is installed in.
49
+ *
50
+ * `internal:LocalNodeModulesPath` holds the arborist location, relative to the
51
+ * root of the tree: `node_modules/a/node_modules/b` for a nested copy and
52
+ * `packages/web/node_modules/b` for a copy under a workspace member. The
53
+ * component's `internal:SrcFile` names a `package-lock.json` in the directory
54
+ * that holds the location's first `node_modules` segment, so the install
55
+ * directory is that location's `node_modules/...` part resolved against the
56
+ * SrcFile's directory. A location without a `node_modules` segment (a
57
+ * workspace member itself) and an absolute location (pnpm metadata found by
58
+ * searching node_modules) name no install a file resolves to.
59
+ *
60
+ * @param {Object} pkg Package component
61
+ * @returns {string|undefined} Absolute install directory
62
+ */
63
+ export function npmInstallDirectory(pkg) {
64
+ const properties = Array.isArray(pkg?.properties) ? pkg.properties : [];
65
+ const location = properties.find(
66
+ (p) => p.name === "internal:LocalNodeModulesPath",
67
+ )?.value;
68
+ const srcFile = properties.find((p) => p.name === "internal:SrcFile")?.value;
69
+ if (!location || !srcFile || isAbsolute(location)) {
70
+ return undefined;
71
+ }
72
+ const normalized = `${location}`.replaceAll("\\", "/");
73
+ let start = -1;
74
+ if (normalized.startsWith("node_modules/")) {
75
+ start = 0;
76
+ } else {
77
+ const nested = normalized.indexOf("/node_modules/");
78
+ start = nested >= 0 ? nested + 1 : -1;
79
+ }
80
+ if (start < 0) {
81
+ return undefined;
82
+ }
83
+ return resolve(dirname(srcFile), normalized.slice(start));
84
+ }
85
+
86
+ /**
87
+ * References the given root components depend on directly.
88
+ *
89
+ * @param {Array<{ref: string, dependsOn?: string[]}>} dependencies Dependency tree
90
+ * @param {Iterable<string>} rootRefs bom-refs of the project and its workspace members
91
+ * @returns {Set<string>} bom-refs of their direct dependencies
92
+ */
93
+ export function directDependencyRefs(dependencies, rootRefs) {
94
+ const roots = new Set(rootRefs || []);
95
+ const direct = new Set();
96
+ for (const dependency of dependencies || []) {
97
+ if (roots.has(dependency?.ref)) {
98
+ for (const ref of dependency.dependsOn || []) {
99
+ direct.add(ref);
100
+ }
101
+ }
102
+ }
103
+ return direct;
104
+ }
105
+
106
+ /**
107
+ * The direct dependencies that apply to a project file, nearest first.
108
+ *
109
+ * A file inside an npm workspace member (a component with
110
+ * `cdx:npm:isWorkspace=true` and its directory in `cdx:npm:resolvedPath`,
111
+ * relative to the project) depends first on what that member declares, then
112
+ * on what the project root declares, since Node.js finds the root's hoisted
113
+ * copies from inside a member too. Any other file depends on the root's.
114
+ *
115
+ * @param {Object[]} components Components or package list
116
+ * @param {Object[]} dependencies Dependency tree
117
+ * @param {string[]} rootRefs bom-refs of the project and the sub-components it lists
118
+ * @returns {function(string): Set<string>[]} Lookup from a file name, relative to the project, to sets of bom-refs
119
+ */
120
+ export function createDirectDependencyLookup(
121
+ components,
122
+ dependencies,
123
+ rootRefs,
124
+ ) {
125
+ const rootDirect = directDependencyRefs(dependencies, rootRefs);
126
+ const members = [];
127
+ for (const comp of components || []) {
128
+ const properties = Array.isArray(comp?.properties) ? comp.properties : [];
129
+ const isWorkspace = properties.some(
130
+ (p) => p.name === "cdx:npm:isWorkspace" && p.value === "true",
131
+ );
132
+ const memberPath = properties.find(
133
+ (p) => p.name === "cdx:npm:resolvedPath",
134
+ )?.value;
135
+ if (!isWorkspace || !memberPath || !comp["bom-ref"]) {
136
+ continue;
137
+ }
138
+ const dir = `${memberPath}`.replaceAll("\\", "/").replace(/\/+$/, "");
139
+ if (!dir || dir.startsWith("..") || isAbsolute(dir)) {
140
+ continue;
141
+ }
142
+ members.push({ dir, ref: comp["bom-ref"] });
143
+ }
144
+ members.sort((a, b) => b.dir.length - a.dir.length);
145
+ const memberDirect = new Map();
146
+ return (fileName) => {
147
+ const file = `${fileName ?? ""}`.replaceAll("\\", "/");
148
+ const member = members.find((m) => file.startsWith(`${m.dir}/`));
149
+ if (!member) {
150
+ return [rootDirect];
151
+ }
152
+ if (!memberDirect.has(member.ref)) {
153
+ memberDirect.set(
154
+ member.ref,
155
+ directDependencyRefs(dependencies, [member.ref]),
156
+ );
157
+ }
158
+ return [memberDirect.get(member.ref), rootDirect];
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Narrow candidate versions to the ones a file's nearest direct dependencies
164
+ * name.
165
+ *
166
+ * @param {Set<string>} candidates Candidate references
167
+ * @param {Set<string>[]} directSets Direct dependency sets, nearest first
168
+ * @param {function(string): string} [toRef] Maps a candidate to its bom-ref
169
+ * @returns {Set<string>} The candidates the nearest set names, or all of them
170
+ */
171
+ function narrowToDirect(candidates, directSets, toRef = (c) => c) {
172
+ for (const direct of directSets) {
173
+ const hits = [...candidates].filter((c) => direct.has(toRef(c)));
174
+ if (hits.length) {
175
+ return new Set(hits);
176
+ }
177
+ }
178
+ return candidates;
179
+ }
180
+
181
+ /**
182
+ * bom-refs of a parent component and the sub-components it lists, which are
183
+ * the roots whose direct dependencies a project's own files use.
184
+ *
185
+ * @param {Object} parentComponent Parent (metadata) component
186
+ * @returns {string[]} bom-refs
187
+ */
188
+ export function projectRootRefs(parentComponent) {
189
+ return [parentComponent, ...(parentComponent?.components || [])]
190
+ .map((c) => c?.["bom-ref"])
191
+ .filter(Boolean);
192
+ }
193
+
194
+ /**
195
+ * Decide which installed versions of an npm package a project file uses.
196
+ *
197
+ * Only package names with more than one version in `pkgList` are decided; a
198
+ * file uses the single version of any other package. For the rest, the file
199
+ * uses:
200
+ *
201
+ * 1. the copy Node.js resolves from the file's directory, when the lock file
202
+ * records install locations (package-lock.json): the first
203
+ * `<dir>/node_modules/<name>` holding one of the versions, walking up from
204
+ * the file;
205
+ * 2. otherwise the versions the file's workspace member, or else the project,
206
+ * depends on directly (see `createDirectDependencyLookup`);
207
+ * 3. otherwise every version, since nothing tells them apart.
208
+ *
209
+ * @param {Object[]} pkgList Package components, duplicates per install location included
210
+ * @param {Object} [context] Project facts
211
+ * @param {string} [context.srcDir] Directory the file names are relative to
212
+ * @param {Object[]} [context.dependencies] Dependency tree
213
+ * @param {string[]} [context.rootRefs] bom-refs of the project and the sub-components it lists
214
+ * @returns {{isAmbiguous: function(Object): boolean, allows: function(Object, string): boolean}} Chooser
215
+ */
216
+ export function createNpmVersionChooser(pkgList, context = {}) {
217
+ const refsByName = new Map();
218
+ const refByInstallDir = new Map();
219
+ for (const pkg of pkgList || []) {
220
+ const ref = pkg?.["bom-ref"];
221
+ if (!ref || !pkg.name) {
222
+ continue;
223
+ }
224
+ if (pkg.purl && npmPackageNameOfPurl(pkg.purl) === undefined) {
225
+ continue;
226
+ }
227
+ const name = npmPackageName(pkg);
228
+ if (!refsByName.has(name)) {
229
+ refsByName.set(name, new Set());
230
+ }
231
+ refsByName.get(name).add(ref);
232
+ const installDir = npmInstallDirectory(pkg);
233
+ if (installDir) {
234
+ refByInstallDir.set(installDir, ref);
235
+ }
236
+ }
237
+ const directFor = createDirectDependencyLookup(
238
+ pkgList,
239
+ context.dependencies,
240
+ context.rootRefs || [],
241
+ );
242
+ const decided = new Map();
243
+ const chosenRefs = (name, fileName) => {
244
+ const candidates = refsByName.get(name);
245
+ const fileDir =
246
+ fileName && context.srcDir
247
+ ? resolve(context.srcDir, dirname(`${fileName}`))
248
+ : undefined;
249
+ const cacheKey = `${name}\u0000${dirname(`${fileName ?? ""}`)}`;
250
+ if (decided.has(cacheKey)) {
251
+ return decided.get(cacheKey);
252
+ }
253
+ let chosen;
254
+ if (fileDir) {
255
+ let dir = fileDir;
256
+ while (!chosen) {
257
+ const ref = refByInstallDir.get(resolve(dir, "node_modules", name));
258
+ if (ref && candidates.has(ref)) {
259
+ chosen = new Set([ref]);
260
+ break;
261
+ }
262
+ const parent = dirname(dir);
263
+ if (parent === dir) {
264
+ break;
265
+ }
266
+ dir = parent;
267
+ }
268
+ }
269
+ if (!chosen) {
270
+ chosen = narrowToDirect(candidates, directFor(fileName));
271
+ }
272
+ decided.set(cacheKey, chosen);
273
+ return chosen;
274
+ };
275
+ const isAmbiguous = (pkg) =>
276
+ Boolean(pkg?.name) && (refsByName.get(npmPackageName(pkg))?.size || 0) > 1;
277
+ return {
278
+ isAmbiguous,
279
+ allows(pkg, fileName) {
280
+ if (!isAmbiguous(pkg)) {
281
+ return true;
282
+ }
283
+ return chosenRefs(npmPackageName(pkg), fileName).has(pkg["bom-ref"]);
284
+ },
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Keep each npm occurrence only on the versions the project file uses.
290
+ *
291
+ * evinse credits a slice record to every component of the record's package,
292
+ * one per installed version. For a package with several versions in the BOM,
293
+ * an occurrence in file F stays on a version when the input BOM's own import
294
+ * evidence (seeded before any slice is read) puts that version in F; when the
295
+ * BOM credits none of the versions in F, or several, the versions F's
296
+ * workspace member or else the project depends on directly are kept (see
297
+ * `createDirectDependencyLookup`); failing both, all stay.
298
+ *
299
+ * @param {Object} purlLocationMap purl -> Set of occurrence keys, changed in place
300
+ * @param {Object} purlSymbolMap purl -> occurrence key -> symbol, changed in place
301
+ * @param {Object} seededLocationMap purl -> Set of occurrence keys the input BOM carried
302
+ * @param {Object} bomJson Input BOM, for its components, dependencies and metadata component
303
+ * @returns {Set<string>} purls left with no occurrence, whose input occurrences belong to another version
304
+ */
305
+ export function keepResolvedNpmVersions(
306
+ purlLocationMap,
307
+ purlSymbolMap,
308
+ seededLocationMap,
309
+ bomJson,
310
+ ) {
311
+ const purlsByName = new Map();
312
+ const refByPurl = new Map();
313
+ for (const comp of bomJson?.components || []) {
314
+ const name = comp?.purl ? npmPackageNameOfPurl(comp.purl) : undefined;
315
+ if (!name) {
316
+ continue;
317
+ }
318
+ if (!purlsByName.has(name)) {
319
+ purlsByName.set(name, new Set());
320
+ }
321
+ purlsByName.get(name).add(comp.purl);
322
+ refByPurl.set(comp.purl, comp["bom-ref"] || comp.purl);
323
+ }
324
+ const clearedPurls = new Set();
325
+ const directFor = createDirectDependencyLookup(
326
+ bomJson?.components,
327
+ bomJson?.dependencies,
328
+ projectRootRefs(bomJson?.metadata?.component),
329
+ );
330
+ for (const purls of purlsByName.values()) {
331
+ if (purls.size < 2) {
332
+ continue;
333
+ }
334
+ const seededIn = new Map();
335
+ for (const purl of purls) {
336
+ for (const key of seededLocationMap?.[purl] || []) {
337
+ const location = occurrenceKeyLocation(key);
338
+ if (!seededIn.has(location)) {
339
+ seededIn.set(location, new Set());
340
+ }
341
+ seededIn.get(location).add(purl);
342
+ }
343
+ }
344
+ const decided = new Map();
345
+ const keeps = (purl, location) => {
346
+ if (!decided.has(location)) {
347
+ let chosen = seededIn.get(location) || purls;
348
+ if (chosen.size > 1) {
349
+ chosen = narrowToDirect(chosen, directFor(location), (purl) =>
350
+ refByPurl.get(purl),
351
+ );
352
+ }
353
+ decided.set(location, chosen);
354
+ }
355
+ return decided.get(location).has(purl);
356
+ };
357
+ for (const purl of purls) {
358
+ const keys = purlLocationMap[purl];
359
+ if (!keys?.size) {
360
+ continue;
361
+ }
362
+ for (const key of [...keys]) {
363
+ if (!keeps(purl, occurrenceKeyLocation(key))) {
364
+ keys.delete(key);
365
+ if (purlSymbolMap?.[purl]) {
366
+ delete purlSymbolMap[purl][key];
367
+ }
368
+ }
369
+ }
370
+ if (!keys.size) {
371
+ clearedPurls.add(purl);
372
+ }
373
+ }
374
+ }
375
+ return clearedPurls;
376
+ }