@cdxgen/cdxgen 13.5.0 → 13.6.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 (182) hide show
  1. package/README.md +6 -6
  2. package/bin/cdxgen.js +4 -0
  3. package/bin/evinse.js +3 -0
  4. package/bin/validate.js +6 -0
  5. package/data/license-aliases.json +24 -2
  6. package/data/remediations.json +124 -0
  7. package/data/rules/ai-bom.yaml +1 -1
  8. package/data/rules/brotli-decoder.yaml +107 -0
  9. package/data/rules/vscode-extensions.yaml +75 -0
  10. package/data/spdx-license-list.json +121 -7
  11. package/data/spdx-licenses.json +69 -0
  12. package/data/spdx.schema.json +845 -2
  13. package/lib/cli/bomAssembly.js +92 -3
  14. package/lib/cli/index.js +108 -1
  15. package/lib/cli/jsBom.js +153 -64
  16. package/lib/cli/jvmBom.js +237 -46
  17. package/lib/cli/managedBom.js +142 -16
  18. package/lib/cli/nativeBom.js +42 -5
  19. package/lib/core/boundedFile.js +344 -0
  20. package/lib/core/brotliProbe.js +182 -0
  21. package/lib/core/env.js +8 -0
  22. package/lib/core/fs.js +12 -0
  23. package/lib/core/httpClient.js +98 -1
  24. package/lib/core/lockPaths.js +38 -0
  25. package/lib/core/paths.js +24 -1
  26. package/lib/core/processTree.js +5 -0
  27. package/lib/core/toolRegistry.js +170 -0
  28. package/lib/ecosystems/cppEvidence.js +263 -106
  29. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  30. package/lib/ecosystems/ecosystems.js +25 -10
  31. package/lib/ecosystems/gemHostProbe.js +301 -0
  32. package/lib/ecosystems/gradleutils.js +828 -224
  33. package/lib/ecosystems/parsers-bazel.js +107 -6
  34. package/lib/ecosystems/parsers-js.js +505 -22
  35. package/lib/ecosystems/parsers-misc.js +209 -20
  36. package/lib/ecosystems/parsers-python.js +89 -2
  37. package/lib/ecosystems/pythonutils.js +128 -2
  38. package/lib/ecosystems/rubyutils.js +538 -31
  39. package/lib/ecosystems/scalacliutils.js +2 -0
  40. package/lib/ecosystems/scalasem.js +796 -151
  41. package/lib/ecosystems/vendoredCode.js +237 -3
  42. package/lib/ecosystems/venvProbe.js +360 -0
  43. package/lib/evinser/evinser.js +346 -43
  44. package/lib/helpers/agenticToolutils.js +413 -93
  45. package/lib/helpers/vsixutils.js +1574 -315
  46. package/lib/inventory/atomUtils.js +77 -0
  47. package/lib/inventory/brotliDecoder.js +142 -0
  48. package/lib/inventory/buildToolRateLimit.js +1 -1
  49. package/lib/inventory/cbomutils.js +56 -7
  50. package/lib/inventory/deps.js +35 -0
  51. package/lib/inventory/dosai.js +139 -5
  52. package/lib/inventory/dotnetRuntime.js +217 -0
  53. package/lib/inventory/envcontext.js +307 -13
  54. package/lib/inventory/evidenceUtils.js +11 -0
  55. package/lib/inventory/fetchBatch.js +1 -1
  56. package/lib/inventory/formulationParsers.js +31 -3
  57. package/lib/inventory/gtfobins.js +13 -0
  58. package/lib/inventory/hostRuntimeProbe.js +686 -0
  59. package/lib/inventory/jvmLocalRepos.js +9 -2
  60. package/lib/inventory/jvmToolEnv.js +29 -0
  61. package/lib/inventory/licenseEnhancer.js +12 -0
  62. package/lib/inventory/mavenCentralMirror.js +534 -0
  63. package/lib/inventory/nodeBuildInfo.js +218 -0
  64. package/lib/inventory/nodeHostProbe.js +202 -0
  65. package/lib/inventory/osPackageResolver.js +182 -0
  66. package/lib/inventory/phpBuildInfo.js +10 -59
  67. package/lib/inventory/phpHostProbe.js +32 -488
  68. package/lib/inventory/rubyBuildInfo.js +972 -0
  69. package/lib/inventory/rubyHostProbe.js +218 -0
  70. package/lib/inventory/scalaCoords.js +4 -3
  71. package/lib/inventory/scalaDedupe.js +440 -0
  72. package/lib/inventory/source.js +49 -2
  73. package/lib/managers/binary.js +337 -3
  74. package/lib/managers/docker.js +79 -0
  75. package/lib/parsers/largeJson.js +28 -11
  76. package/lib/parsers/vscodeExtensionId.js +183 -0
  77. package/lib/server/server.js +12 -1
  78. package/lib/stages/postgen/ruleEngine.js +13 -5
  79. package/lib/stages/pregen/envAudit.js +1 -0
  80. package/lib/stages/pregen/pregen.js +114 -57
  81. package/lib/validator/bomValidator.js +6 -1
  82. package/lib/validator/generated/validate-1.6.mjs +1 -1
  83. package/lib/validator/generated/validate-1.7.mjs +1 -1
  84. package/lib/validator/generated/validate-2.0.mjs +1 -1
  85. package/lib/validator/index.js +1 -1
  86. package/package.json +3 -2
  87. package/types/lib/cli/bomAssembly.d.ts +13 -1
  88. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  89. package/types/lib/cli/index.d.ts.map +1 -1
  90. package/types/lib/cli/jsBom.d.ts +1 -17
  91. package/types/lib/cli/jsBom.d.ts.map +1 -1
  92. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  93. package/types/lib/cli/managedBom.d.ts.map +1 -1
  94. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  95. package/types/lib/core/boundedFile.d.ts +70 -0
  96. package/types/lib/core/boundedFile.d.ts.map +1 -0
  97. package/types/lib/core/brotliProbe.d.ts +37 -0
  98. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  99. package/types/lib/core/env.d.ts +4 -0
  100. package/types/lib/core/env.d.ts.map +1 -1
  101. package/types/lib/core/fs.d.ts.map +1 -1
  102. package/types/lib/core/httpClient.d.ts +24 -0
  103. package/types/lib/core/httpClient.d.ts.map +1 -1
  104. package/types/lib/core/lockPaths.d.ts +19 -0
  105. package/types/lib/core/lockPaths.d.ts.map +1 -0
  106. package/types/lib/core/processTree.d.ts +2 -0
  107. package/types/lib/core/processTree.d.ts.map +1 -1
  108. package/types/lib/core/toolRegistry.d.ts +36 -0
  109. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  110. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  111. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  112. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  113. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  114. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  115. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  116. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  117. package/types/lib/ecosystems/gradleutils.d.ts +47 -13
  118. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  121. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
  123. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-python.d.ts +10 -0
  125. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  126. package/types/lib/ecosystems/pythonutils.d.ts +20 -2
  127. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  128. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  129. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  131. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  132. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  133. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  134. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  135. package/types/lib/evinser/evinser.d.ts +63 -8
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/inventory/atomUtils.d.ts +19 -0
  138. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  139. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  140. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  141. package/types/lib/inventory/cbomutils.d.ts +17 -0
  142. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  143. package/types/lib/inventory/deps.d.ts.map +1 -1
  144. package/types/lib/inventory/dosai.d.ts +17 -1
  145. package/types/lib/inventory/dosai.d.ts.map +1 -1
  146. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  147. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  148. package/types/lib/inventory/envcontext.d.ts +52 -6
  149. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  150. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  151. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  152. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  153. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  154. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  155. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  156. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  157. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  158. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  159. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  160. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  161. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  162. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  163. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  164. package/types/lib/inventory/osPackageResolver.d.ts +38 -0
  165. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  166. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
  167. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
  168. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  169. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  170. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  171. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  172. package/types/lib/inventory/source.d.ts +10 -0
  173. package/types/lib/inventory/source.d.ts.map +1 -1
  174. package/types/lib/managers/binary.d.ts.map +1 -1
  175. package/types/lib/managers/docker.d.ts +12 -0
  176. package/types/lib/managers/docker.d.ts.map +1 -1
  177. package/types/lib/parsers/largeJson.d.ts +4 -3
  178. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  179. package/types/lib/server/server.d.ts.map +1 -1
  180. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  181. package/types/lib/stages/pregen/pregen.d.ts +12 -8
  182. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
@@ -0,0 +1,218 @@
1
+ // Bounded, read-only probe for the Ruby no package manager owns on a live host,
2
+ // for `-t os` / obom. osquery's package tables list what dpkg, rpm and the like
3
+ // installed, so a Ruby built by ruby-build, rbenv, rvm, asdf, mise, chruby or
4
+ // frum, unpacked from a toolcache tarball, or installed under /usr/local or
5
+ // /opt is invisible to them, and with it the version an advisory in the
6
+ // interpreter core is read against.
7
+ //
8
+ // Candidates come from fixed directories and from each version manager's tree
9
+ // under the home directories, one wildcard level at a time, never from a walk
10
+ // of the file system. The ownership rules, caps and degradations are the
11
+ // shared host-runtime probe's. Only rbconfig.rb and the interpreter's own
12
+ // files are read; nothing is executed.
13
+
14
+ import { join } from "node:path";
15
+
16
+ import {
17
+ isDryRun,
18
+ readEnvironmentVariable,
19
+ recordActivity,
20
+ } from "../core/activity.js";
21
+ import { recordDegradation } from "../core/buildLedger.js";
22
+ import {
23
+ isDirectory,
24
+ listDirectory,
25
+ probeHostRuntime,
26
+ } from "./hostRuntimeProbe.js";
27
+ import {
28
+ attachRubyBuild,
29
+ createRubyRuntimeInventory,
30
+ isRubyBinaryName,
31
+ MAX_RUBY_PROBES,
32
+ readRubyBuildVersion,
33
+ } from "./rubyBuildInfo.js";
34
+
35
+ // The most directories one wildcard pattern may expand to.
36
+ const MAX_EXPANDED_DIRECTORIES = 256;
37
+
38
+ // Relative to the root.
39
+ const POSIX_DIRECTORIES = {
40
+ linux: [
41
+ "usr/local/bin",
42
+ "opt/*/bin",
43
+ "opt/rubies/*/bin",
44
+ "opt/hostedtoolcache/Ruby/*/*/bin",
45
+ "usr/local/rbenv/versions/*/bin",
46
+ "usr/local/rvm/rubies/*/bin",
47
+ ],
48
+ darwin: [
49
+ "usr/local/bin",
50
+ "opt/*/bin",
51
+ "opt/rubies/*/bin",
52
+ "Users/runner/hostedtoolcache/Ruby/*/*/bin",
53
+ ],
54
+ };
55
+
56
+ // Relative to a home directory.
57
+ export const RUBY_VERSION_MANAGER_DIRECTORIES = [
58
+ ".rbenv/versions/*/bin",
59
+ ".rvm/rubies/*/bin",
60
+ ".asdf/installs/ruby/*/bin",
61
+ ".local/share/mise/installs/ruby/*/bin",
62
+ ".rubies/*/bin",
63
+ ".frum/versions/*/bin",
64
+ ".local/share/rv/rubies/*/bin",
65
+ ];
66
+
67
+ export const HOME_PARENTS = {
68
+ linux: ["home"],
69
+ darwin: ["Users"],
70
+ };
71
+
72
+ /**
73
+ * Expand a relative directory pattern whose `*` segments match one directory
74
+ * name each.
75
+ *
76
+ * @param {string} base absolute directory the pattern hangs off
77
+ * @param {string} pattern `/`-separated relative pattern
78
+ * @returns {string[]} existing directories, at most MAX_EXPANDED_DIRECTORIES
79
+ */
80
+ export function expandDirectories(base, pattern) {
81
+ let current = [base];
82
+ for (const segment of pattern.split("/")) {
83
+ const next = [];
84
+ for (const directory of current) {
85
+ if (segment !== "*") {
86
+ next.push(join(directory, segment));
87
+ continue;
88
+ }
89
+ for (const name of listDirectory(directory)) {
90
+ const child = join(directory, name);
91
+ if (isDirectory(child)) {
92
+ next.push(child);
93
+ }
94
+ if (next.length >= MAX_EXPANDED_DIRECTORIES) {
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ current = next.slice(0, MAX_EXPANDED_DIRECTORIES);
100
+ }
101
+ return current.filter(isDirectory);
102
+ }
103
+
104
+ /**
105
+ * The home directories of a host: /root and /home/* on Linux, /Users/* on
106
+ * macOS.
107
+ *
108
+ * @param {{rootPath: string, platform: string}} context probe context
109
+ * @returns {string[]}
110
+ */
111
+ export function homeDirectories(context) {
112
+ const { rootPath, platform } = context;
113
+ const homes = [];
114
+ if (platform === "linux") {
115
+ homes.push(join(rootPath, "root"));
116
+ }
117
+ for (const parent of HOME_PARENTS[platform] || []) {
118
+ for (const name of listDirectory(join(rootPath, parent))) {
119
+ const home = join(rootPath, parent, name);
120
+ if (isDirectory(home)) {
121
+ homes.push(home);
122
+ }
123
+ }
124
+ }
125
+ return homes.slice(0, MAX_EXPANDED_DIRECTORIES);
126
+ }
127
+
128
+ /**
129
+ * The directories a Windows Ruby lives in: the RubyInstaller's C:\Ruby34-x64,
130
+ * Chocolatey's C:\tools\ruby34, and the hosted toolcache.
131
+ *
132
+ * @param {Object} context probe context
133
+ * @returns {string[]} install roots, each with a `bin` directory
134
+ */
135
+ export function windowsRubyRoots(context) {
136
+ const { rootPath } = context;
137
+ const roots = [];
138
+ for (const parent of [rootPath, join(rootPath, "tools")]) {
139
+ for (const name of listDirectory(parent)) {
140
+ const directory = join(parent, name);
141
+ if (name.toLowerCase().startsWith("ruby") && isDirectory(directory)) {
142
+ roots.push(directory);
143
+ }
144
+ }
145
+ }
146
+ roots.push(
147
+ ...expandDirectories(rootPath, "hostedtoolcache/windows/Ruby/*/*"),
148
+ );
149
+ return roots.slice(0, MAX_EXPANDED_DIRECTORIES);
150
+ }
151
+
152
+ function candidateDirectories(context) {
153
+ const { rootPath, platform } = context;
154
+ const directories = [];
155
+ if (platform === "win32") {
156
+ for (const root of windowsRubyRoots(context)) {
157
+ directories.push({ directory: join(root, "bin") });
158
+ }
159
+ return directories;
160
+ }
161
+ for (const pattern of POSIX_DIRECTORIES[platform] || []) {
162
+ for (const directory of expandDirectories(rootPath, pattern)) {
163
+ directories.push({ directory });
164
+ }
165
+ }
166
+ for (const home of homeDirectories(context)) {
167
+ for (const pattern of RUBY_VERSION_MANAGER_DIRECTORIES) {
168
+ for (const directory of expandDirectories(home, pattern)) {
169
+ directories.push({ directory });
170
+ }
171
+ }
172
+ }
173
+ return directories;
174
+ }
175
+
176
+ /**
177
+ * Look for Ruby no package manager owns on the host. Components come back
178
+ * only for the files nothing owns: a `file` component for each `ruby` binary
179
+ * with the cdx:ruby properties, and one `platform` component per Ruby version
180
+ * with its CPE.
181
+ *
182
+ * @param {Object} [options] probe options, as probeHostPhp takes them
183
+ * @returns {{components: Object[], dependencies: Object[]}}
184
+ */
185
+ export function probeHostRuby(options = {}) {
186
+ return probeHostRuntime(options, {
187
+ label: "Ruby",
188
+ ecosystem: "ruby",
189
+ capability: "ruby-host-probe",
190
+ remediationId: "ruby.host-probe.package-db-unreadable",
191
+ maxProbes: MAX_RUBY_PROBES,
192
+ chocolateyPackages: ["ruby"],
193
+ // Chocolatey's ruby package runs the RubyInstaller into C:\tools.
194
+ chocolateyDirectories: (context) => [join(context.rootPath, "tools")],
195
+ directories: candidateDirectories,
196
+ classify: (name, platform) => {
197
+ if (platform === "win32") {
198
+ return name.toLowerCase() === "ruby.exe" ? "ruby" : undefined;
199
+ }
200
+ return isRubyBinaryName(name) ? "ruby" : undefined;
201
+ },
202
+ readBuild: (context, candidate) =>
203
+ readRubyBuildVersion(candidate.real, context.realRoot),
204
+ createInventory: createRubyRuntimeInventory,
205
+ attach: (inventory, component, build) =>
206
+ attachRubyBuild(inventory, component, build),
207
+ finish: (inventory) => ({
208
+ components: inventory.runtimeComponents,
209
+ dependencies: inventory.dependencies,
210
+ }),
211
+ hooks: {
212
+ isDryRun,
213
+ readEnvironmentVariable,
214
+ recordActivity,
215
+ recordDegradation,
216
+ },
217
+ });
218
+ }
@@ -7,7 +7,7 @@ import { Purl } from "@cdxgen/cdx-purl";
7
7
  // platform suffix (`jwt-core`, `upickle_sjs1`), so the same library is named
8
8
  // differently depending on the build tool that reported it.
9
9
  const SCALA_BINARY_SUFFIX_REGEX = /_(2\.\d+|3)$/;
10
- const SCALA_PLATFORM_SUFFIX_REGEX = /_(sjs1|sjs0\.6|native0\.\d+)$/;
10
+ const SCALA_PLATFORM_SUFFIX_REGEX = /_(sjs\d+(?:\.\d+)?|native\d+(?:\.\d+)?)$/;
11
11
 
12
12
  /**
13
13
  * Parse a Scala-published Maven artifact name into its coordinate parts.
@@ -123,10 +123,11 @@ export function scalaCoordinateOfPurl(purlString) {
123
123
  }
124
124
 
125
125
  // An artifact name that already ends in a full Scala version, as scala-cli
126
- // `:::` and sbt `CrossVersion.full` artifacts do (`_3.3.7`, `_2.13.16`). A
126
+ // `:::` and sbt `CrossVersion.full` artifacts do (`_3.3.7`, `_2.13.16`,
127
+ // `_3.4.0-RC1-bin-20231010-abc1234-NIGHTLY`). A
127
128
  // binary suffix such as `_3` left on a name is part of the name: the
128
129
  // component producers strip exactly one, into the compiler version.
129
- const SCALA_FULL_VERSION_SUFFIX_REGEX = /_\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?$/;
130
+ const SCALA_FULL_VERSION_SUFFIX_REGEX = /_\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/;
130
131
 
131
132
  /**
132
133
  * The artifactId the library is published under, for cache and repository
@@ -0,0 +1,440 @@
1
+ import { basename } from "node:path";
2
+
3
+ import { Purl } from "@cdxgen/cdx-purl";
4
+
5
+ import { mergeDependencies, trimComponents } from "./depsUtils.js";
6
+ import { mavenPurl } from "./purl.js";
7
+ import { publishedArtifactId, scalaCoordinateOfPurl } from "./scalaCoords.js";
8
+
9
+ const COMPILER_VERSION_PROPERTY = "cdx:scala:compilerVersion";
10
+
11
+ // A jar scan that read the coordinates from the jar's own pom.properties
12
+ // records them with this confidence; a guess from the file name or the
13
+ // manifest records less.
14
+ const DESCRIPTOR_CONFIDENCE = 0.5;
15
+
16
+ function compilerVersionsOf(component) {
17
+ return (component.properties || [])
18
+ .filter((property) => property.name === COMPILER_VERSION_PROPERTY)
19
+ .map((property) => property.value)
20
+ .filter(Boolean);
21
+ }
22
+
23
+ function parsedPurl(component) {
24
+ try {
25
+ return Purl.parse(component?.purl || "");
26
+ } catch (_err) {
27
+ return undefined;
28
+ }
29
+ }
30
+
31
+ // The jar names a component was read from, without the extension.
32
+ function jarStems(component) {
33
+ return (component.properties || [])
34
+ .filter((property) => property.name === "internal:SrcFile")
35
+ .map((property) => basename(String(property.value || "")))
36
+ .filter((name) => name.toLowerCase().endsWith(".jar"))
37
+ .map((name) => name.slice(0, -".jar".length));
38
+ }
39
+
40
+ function isJarSourced(component) {
41
+ return jarStems(component).length > 0;
42
+ }
43
+
44
+ // Whether a jar scan guessed the coordinates rather than reading them from the
45
+ // jar's Maven descriptor. A jar with a descriptor names its own artifact, even
46
+ // when that artifact is spelled `<group>.<name>` like a staged copy.
47
+ function hasGuessedIdentity(component) {
48
+ const identities = [component.evidence?.identity || []].flat();
49
+ return !identities.some(
50
+ (identity) =>
51
+ (identity?.confidence ?? 0) >= DESCRIPTOR_CONFIDENCE ||
52
+ (identity?.methods || []).some(
53
+ (method) => (method?.confidence ?? 0) >= DESCRIPTOR_CONFIDENCE,
54
+ ),
55
+ );
56
+ }
57
+
58
+ // A classifier or a type other than jar names a different artifact of the same
59
+ // coordinates, which is not a duplicate.
60
+ function isPlainJar(component) {
61
+ const purl = parsedPurl(component);
62
+ if (!purl) {
63
+ return false;
64
+ }
65
+ const qualifiers = purl.qualifiers || {};
66
+ return (
67
+ !qualifiers.classifier && (!qualifiers.type || qualifiers.type === "jar")
68
+ );
69
+ }
70
+
71
+ // The Scala binary versions a component was built for: the suffix its name
72
+ // keeps, or the compiler versions a build tool recorded. Empty for a library
73
+ // that is not a Scala artifact.
74
+ function binaryVersionsOf(component) {
75
+ const suffixVersion = scalaCoordinateOfPurl(component.purl)?.binaryVersion;
76
+ return new Set(
77
+ suffixVersion ? [suffixVersion] : compilerVersionsOf(component),
78
+ );
79
+ }
80
+
81
+ // Two spellings name one artifact only when they were built for the same Scala
82
+ // binary version, or both for none. `kafka_2.12` and `kafka_2.13` share the
83
+ // normalized coordinates but are different artifacts.
84
+ function sameBinaryVersion(a, b) {
85
+ const left = binaryVersionsOf(a);
86
+ const right = binaryVersionsOf(b);
87
+ if (!left.size || !right.size) {
88
+ return !left.size && !right.size;
89
+ }
90
+ return [...left].some((version) => right.has(version));
91
+ }
92
+
93
+ // Lower is better. A component a build tool reported outranks one a jar scan
94
+ // named; then the name without a `<group>.` prefix, then the purl that names
95
+ // its repository, then the shorter purl, then the lexicographically first, so
96
+ // the choice never depends on the order of the BOM.
97
+ function rank(component) {
98
+ return [
99
+ isJarSourced(component) ? 1 : 0,
100
+ component.name?.startsWith(`${component.group}.`) ? 1 : 0,
101
+ component.purl.includes("repository_url=") ? 0 : 1,
102
+ component.purl.length,
103
+ component.purl,
104
+ ];
105
+ }
106
+
107
+ function isBetter(candidate, component) {
108
+ const a = rank(candidate);
109
+ const b = rank(component);
110
+ for (let i = 0; i < a.length; i++) {
111
+ if (a[i] !== b[i]) {
112
+ return a[i] < b[i];
113
+ }
114
+ }
115
+ return false;
116
+ }
117
+
118
+ function best(components) {
119
+ let chosen;
120
+ for (const component of components) {
121
+ if (!chosen || isBetter(component, chosen)) {
122
+ chosen = component;
123
+ }
124
+ }
125
+ return chosen;
126
+ }
127
+
128
+ function refOf(component) {
129
+ return component["bom-ref"] || component.purl;
130
+ }
131
+
132
+ // The file names sbt-native-packager gives a staged copy of a library,
133
+ // without the version: `<group>.<artifact>` with the artifact as published,
134
+ // once for each Scala binary version it was built for, and once as named,
135
+ // since a module of the build itself is staged without the suffix when it
136
+ // turns cross paths off.
137
+ function stagedStems(component) {
138
+ return [
139
+ ...new Set([
140
+ `${component.group}.${component.name}`,
141
+ ...compilerVersionsOf(component).map(
142
+ (version) =>
143
+ `${component.group}.${publishedArtifactId({
144
+ name: component.name,
145
+ properties: [{ name: COMPILER_VERSION_PROPERTY, value: version }],
146
+ })}`,
147
+ ),
148
+ ]),
149
+ ];
150
+ }
151
+
152
+ // The listed libraries a staged file name can be a copy of, with the version
153
+ // the name carries: every split of the name at a hyphen whose left side is the
154
+ // staged stem of a library.
155
+ function stagedOwners(stem, ownersByStem) {
156
+ const found = [];
157
+ for (
158
+ let index = stem.indexOf("-");
159
+ index > 0;
160
+ index = stem.indexOf("-", index + 1)
161
+ ) {
162
+ const owners = ownersByStem.get(stem.slice(0, index));
163
+ if (owners) {
164
+ found.push({ owners, version: stem.slice(index + 1) });
165
+ }
166
+ }
167
+ return found;
168
+ }
169
+
170
+ function rewriteReferences(dependencies, refMap) {
171
+ if (!refMap.size) {
172
+ return dependencies;
173
+ }
174
+ const rewritten = (dependencies || []).map((dependency) => {
175
+ const ref = refMap.get(dependency.ref) || dependency.ref;
176
+ return {
177
+ ...dependency,
178
+ ref,
179
+ ...(dependency.dependsOn
180
+ ? {
181
+ dependsOn: [
182
+ ...new Set(
183
+ dependency.dependsOn.map(
184
+ (dependsOn) => refMap.get(dependsOn) || dependsOn,
185
+ ),
186
+ ),
187
+ ].filter((dependsOn) => dependsOn !== ref),
188
+ }
189
+ : {}),
190
+ };
191
+ });
192
+ return mergeDependencies([], rewritten);
193
+ }
194
+
195
+ /**
196
+ * Collapse the components that stand for one Scala library.
197
+ *
198
+ * A build tool such as sbt reports a library as `group:name@version` with the
199
+ * Scala binary suffix moved into `cdx:scala:compilerVersion`. The jars a scan
200
+ * finds on disk name the same library again, and not in the same way: with the
201
+ * suffix (`cats-core_3`), without a repository, or, for a jar without a Maven
202
+ * descriptor, only as the staged file name `<group>.<artifact>-<version>.jar`,
203
+ * which a filename-based guess splits at the wrong dot
204
+ * (`pkg:maven/cpg2-protos/io.appthreat.cpg2-protos_3`). Each of those is merged
205
+ * into the component the build tool reported. A staged jar of another version
206
+ * of a reported library, such as a stale build, keeps its own component but is
207
+ * renamed to the coordinates the reported library has.
208
+ *
209
+ * Spellings of the same coordinates merge only when they were built for the
210
+ * same Scala binary version, so the `_2.12` and `_2.13` artifacts of one
211
+ * library stay two components. Components that no reported library accounts
212
+ * for are left as they are.
213
+ *
214
+ * @param {Object[]} components BOM components
215
+ * @param {Object[]} dependencies BOM dependencies
216
+ * @returns {{components: Object[], dependencies: Object[], purlMap: Map<string, string>}}
217
+ * The collapsed components, the dependencies with references to merged
218
+ * components pointed at the component that remains, and the purl each
219
+ * merged or renamed component now has, keyed by the purl it had
220
+ */
221
+ export function mergeScalaDuplicateComponents(components, dependencies) {
222
+ const purlMap = new Map();
223
+ const candidates = (components || []).filter(
224
+ (component) =>
225
+ component?.purl?.startsWith("pkg:maven/") &&
226
+ isPlainJar(component) &&
227
+ component.group &&
228
+ component.version,
229
+ );
230
+ if (candidates.length < 2) {
231
+ return { components, dependencies, purlMap };
232
+ }
233
+ const byKey = new Map();
234
+ const ownersByStem = new Map();
235
+ for (const component of candidates) {
236
+ const key = scalaCoordinateOfPurl(component.purl)?.key;
237
+ if (key) {
238
+ byKey.set(key, [...(byKey.get(key) || []), component]);
239
+ }
240
+ for (const stem of stagedStems(component)) {
241
+ ownersByStem.set(stem, [...(ownersByStem.get(stem) || []), component]);
242
+ }
243
+ }
244
+ // Which component each one is a duplicate of.
245
+ const duplicateOf = new Map();
246
+ const repoints = new Map();
247
+ for (const component of candidates) {
248
+ const matches = new Set();
249
+ // The same coordinates, spelled with or without the Scala suffix, with or
250
+ // without a repository.
251
+ for (const other of byKey.get(scalaCoordinateOfPurl(component.purl)?.key) ||
252
+ []) {
253
+ if (other !== component && sameBinaryVersion(component, other)) {
254
+ matches.add(other);
255
+ }
256
+ }
257
+ // The staged file name of another listed library, whatever group and name
258
+ // the guess made of it. A jar that names itself in its Maven descriptor is
259
+ // the artifact it says it is.
260
+ if (isJarSourced(component) && hasGuessedIdentity(component)) {
261
+ for (const stem of jarStems(component)) {
262
+ for (const { owners, version } of stagedOwners(stem, ownersByStem)) {
263
+ const sameVersion = owners.filter(
264
+ (owner) => owner !== component && owner.version === version,
265
+ );
266
+ if (sameVersion.length) {
267
+ for (const owner of sameVersion) {
268
+ matches.add(owner);
269
+ }
270
+ continue;
271
+ }
272
+ // Another version of a library a build tool reported, when the guess
273
+ // agrees on it. A version that extends a known one
274
+ // (`4.1.100.Final-linux-x86_64`) can as well be that version with a
275
+ // classifier, so it is left alone.
276
+ const reportedOwners = owners.filter(
277
+ (owner) => owner !== component && !isJarSourced(owner),
278
+ );
279
+ const extendsKnown = owners.some((owner) =>
280
+ version.startsWith(`${owner.version}-`),
281
+ );
282
+ if (
283
+ reportedOwners.length &&
284
+ version === component.version &&
285
+ /^\d/.test(version) &&
286
+ !extendsKnown
287
+ ) {
288
+ const owner = best(reportedOwners);
289
+ const known = repoints.get(component);
290
+ if (!known || isBetter(owner, known)) {
291
+ repoints.set(component, owner);
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ const target = best(
298
+ [...matches].filter((other) => isBetter(other, component)),
299
+ );
300
+ if (target) {
301
+ duplicateOf.set(component, target);
302
+ }
303
+ }
304
+ // A chain of merges never carries a component past one built for another
305
+ // Scala binary version.
306
+ const root = (component) => {
307
+ let current = component;
308
+ while (duplicateOf.has(current)) {
309
+ const next = duplicateOf.get(current);
310
+ if (current !== component && !sameBinaryVersion(component, next)) {
311
+ break;
312
+ }
313
+ current = next;
314
+ }
315
+ return current;
316
+ };
317
+ const refMap = new Map();
318
+ const survivors = new Set(components);
319
+ for (const [component] of duplicateOf) {
320
+ const target = root(component);
321
+ const duplicate = { ...component };
322
+ const targetRef = refOf(target);
323
+ if (refOf(component) && targetRef && refOf(component) !== targetRef) {
324
+ refMap.set(refOf(component), targetRef);
325
+ }
326
+ if (component.purl !== target.purl) {
327
+ purlMap.set(component.purl, target.purl);
328
+ }
329
+ duplicate.purl = target.purl;
330
+ duplicate["bom-ref"] = target["bom-ref"];
331
+ trimComponents([target, duplicate]);
332
+ survivors.delete(component);
333
+ }
334
+ for (const [component, owner] of repoints) {
335
+ if (survivors.has(component)) {
336
+ repointToAnchor(component, root(owner), refMap, purlMap);
337
+ }
338
+ }
339
+ // A component merged into one that was renamed afterwards follows it.
340
+ resolveChains(refMap);
341
+ resolveChains(purlMap);
342
+ // Repointed components can now spell the same coordinates twice.
343
+ return {
344
+ components: trimComponents([...survivors]),
345
+ dependencies: rewriteReferences(dependencies, refMap),
346
+ purlMap,
347
+ };
348
+ }
349
+
350
+ function resolveChains(map) {
351
+ for (const [key, value] of map) {
352
+ let current = value;
353
+ const seen = new Set([key]);
354
+ while (map.has(current) && !seen.has(current)) {
355
+ seen.add(current);
356
+ current = map.get(current);
357
+ }
358
+ map.set(key, current);
359
+ }
360
+ }
361
+
362
+ // Give a staged jar of another version the group, name and purl shape the
363
+ // reported library has, so it is the same library at an older or newer version
364
+ // and not a differently named one.
365
+ function repointToAnchor(component, anchor, refMap, purlMap) {
366
+ const anchorPurl = parsedPurl(anchor);
367
+ if (!anchorPurl) {
368
+ return;
369
+ }
370
+ // The repository the reported version came from says nothing about where
371
+ // this one came from.
372
+ const { repository_url: _repository, ...qualifiers } =
373
+ anchorPurl.qualifiers || {};
374
+ const purl = mavenPurl(
375
+ anchor.group,
376
+ anchorPurl.name,
377
+ component.version,
378
+ qualifiers,
379
+ );
380
+ if (!purl) {
381
+ return;
382
+ }
383
+ const previousRef = refOf(component);
384
+ purlMap.set(component.purl, purl);
385
+ component.group = anchor.group;
386
+ component.name = anchor.name;
387
+ component.purl = purl;
388
+ component["bom-ref"] = decodeURIComponent(purl);
389
+ const known = new Set(compilerVersionsOf(component));
390
+ component.properties = [
391
+ ...(component.properties || []),
392
+ ...(anchor.properties || []).filter(
393
+ (property) =>
394
+ property.name === COMPILER_VERSION_PROPERTY &&
395
+ !known.has(property.value),
396
+ ),
397
+ ];
398
+ if (previousRef && previousRef !== component["bom-ref"]) {
399
+ refMap.set(previousRef, component["bom-ref"]);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Key the jar namespace map by the purls the components have after
405
+ * {@link mergeScalaDuplicateComponents}, so a merged or renamed jar still
406
+ * finds its namespaces. A purl that already has an entry keeps it, with the
407
+ * namespaces of the merged entry added.
408
+ *
409
+ * @param {Object} mapping Jar namespace map keyed by purl
410
+ * @param {Map<string, string>} purlMap New purl keyed by old purl
411
+ * @returns {Object} The re-keyed map
412
+ */
413
+ export function rekeyNamespaceMapping(mapping, purlMap) {
414
+ if (!mapping || !purlMap?.size) {
415
+ return mapping;
416
+ }
417
+ const rekeyed = { ...mapping };
418
+ for (const [oldPurl, newPurl] of purlMap) {
419
+ const entry = rekeyed[oldPurl];
420
+ if (!entry || oldPurl === newPurl) {
421
+ continue;
422
+ }
423
+ delete rekeyed[oldPurl];
424
+ const existing = rekeyed[newPurl];
425
+ if (!existing) {
426
+ rekeyed[newPurl] = entry;
427
+ continue;
428
+ }
429
+ rekeyed[newPurl] = {
430
+ ...existing,
431
+ namespaces: [
432
+ ...new Set([
433
+ ...(existing.namespaces || []),
434
+ ...(entry.namespaces || []),
435
+ ]),
436
+ ],
437
+ };
438
+ }
439
+ return rekeyed;
440
+ }