@cdxgen/cdxgen 13.4.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 (305) hide show
  1. package/README.md +9 -9
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +29 -5
  4. package/bin/evinse.js +8 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/bin/validate.js +6 -0
  8. package/data/cbomosdb-queries.json +13 -1
  9. package/data/component-tags.json +8 -0
  10. package/data/license-aliases.json +24 -2
  11. package/data/queries-darwin.json +25 -1
  12. package/data/queries-win.json +19 -1
  13. package/data/queries.json +41 -0
  14. package/data/remediations.json +407 -1
  15. package/data/rules/ai-bom.yaml +1 -1
  16. package/data/rules/brotli-decoder.yaml +107 -0
  17. package/data/rules/cbom-security.yaml +235 -4
  18. package/data/rules/go-binaries.yaml +46 -0
  19. package/data/rules/golem-go.yaml +80 -0
  20. package/data/rules/hbom-compliance.yaml +40 -22
  21. package/data/rules/hbom-performance.yaml +7 -1
  22. package/data/rules/host-topology.yaml +1 -1
  23. package/data/rules/obom-runtime.yaml +31 -2
  24. package/data/rules/php-runtime.yaml +143 -0
  25. package/data/rules/vscode-extensions.yaml +75 -0
  26. package/data/spdx-license-list.json +121 -7
  27. package/data/spdx-licenses.json +69 -0
  28. package/data/spdx.schema.json +845 -2
  29. package/lib/audit/index.js +14 -0
  30. package/lib/cli/bomAssembly.js +120 -6
  31. package/lib/cli/cliOptions.js +110 -3
  32. package/lib/cli/index.js +182 -6
  33. package/lib/cli/jsBom.js +153 -64
  34. package/lib/cli/jvmBom.js +363 -66
  35. package/lib/cli/managedBom.js +853 -87
  36. package/lib/cli/nativeBom.js +267 -33
  37. package/lib/core/boundedFile.js +344 -0
  38. package/lib/core/brotliProbe.js +182 -0
  39. package/lib/core/env.js +38 -8
  40. package/lib/core/fs.js +40 -19
  41. package/lib/core/httpClient.js +172 -46
  42. package/lib/core/lockPaths.js +38 -0
  43. package/lib/core/paths.js +24 -1
  44. package/lib/core/processTree.js +5 -0
  45. package/lib/core/projectConfig.js +1 -0
  46. package/lib/core/projectTooling.js +97 -0
  47. package/lib/core/propertySanitizer.js +72 -0
  48. package/lib/core/toolRegistry.js +170 -0
  49. package/lib/ecosystems/cppEvidence.js +263 -106
  50. package/lib/ecosystems/cppIncludeResolver.js +16 -5
  51. package/lib/ecosystems/ecosystems.js +25 -10
  52. package/lib/ecosystems/gemHostProbe.js +301 -0
  53. package/lib/ecosystems/golem.js +550 -6
  54. package/lib/ecosystems/gradleutils.js +858 -244
  55. package/lib/ecosystems/parsers-bazel.js +107 -6
  56. package/lib/ecosystems/parsers-dotnet.js +117 -18
  57. package/lib/ecosystems/parsers-go.js +1090 -367
  58. package/lib/ecosystems/parsers-js.js +505 -22
  59. package/lib/ecosystems/parsers-jvm.js +259 -17
  60. package/lib/ecosystems/parsers-misc.js +438 -64
  61. package/lib/ecosystems/parsers-python.js +100 -12
  62. package/lib/ecosystems/pythonutils.js +128 -2
  63. package/lib/ecosystems/rubyutils.js +544 -34
  64. package/lib/ecosystems/rusi.js +10 -0
  65. package/lib/ecosystems/scalacliutils.js +4 -1
  66. package/lib/ecosystems/scalasem.js +798 -151
  67. package/lib/ecosystems/vendoredCode.js +237 -3
  68. package/lib/ecosystems/venvProbe.js +360 -0
  69. package/lib/evinser/evinser.js +472 -61
  70. package/lib/evinser/swiftsem.js +68 -5
  71. package/lib/helpers/agenticToolutils.js +413 -93
  72. package/lib/helpers/vsixutils.js +1574 -315
  73. package/lib/inventory/agentSkillSpec.js +17 -0
  74. package/lib/inventory/apkInstalledDb.js +243 -0
  75. package/lib/inventory/atomUtils.js +110 -0
  76. package/lib/inventory/auditCategories.js +3 -3
  77. package/lib/inventory/brotliDecoder.js +142 -0
  78. package/lib/inventory/buildToolRateLimit.js +1 -1
  79. package/lib/inventory/cacheDir.js +52 -9
  80. package/lib/inventory/cbomutils.js +56 -7
  81. package/lib/inventory/certificates.js +2371 -0
  82. package/lib/inventory/communityAiConfigParser.js +4 -9
  83. package/lib/inventory/deps.js +41 -4
  84. package/lib/inventory/display.js +200 -3
  85. package/lib/inventory/dosai.js +639 -63
  86. package/lib/inventory/dotnetRuntime.js +217 -0
  87. package/lib/inventory/dynamic.js +45 -3
  88. package/lib/inventory/envcontext.js +475 -18
  89. package/lib/inventory/evidenceUtils.js +447 -23
  90. package/lib/inventory/fetchBatch.js +1 -1
  91. package/lib/inventory/formulationParsers.js +32 -3
  92. package/lib/inventory/goBuildInfo.js +140 -0
  93. package/lib/inventory/gtfobins.js +13 -0
  94. package/lib/inventory/hbom.js +67 -7
  95. package/lib/inventory/hbomAnalysis.js +16 -0
  96. package/lib/inventory/hostRuntimeProbe.js +686 -0
  97. package/lib/inventory/hostTopology.js +21 -0
  98. package/lib/inventory/jvmLocalRepos.js +9 -2
  99. package/lib/inventory/jvmToolEnv.js +29 -0
  100. package/lib/inventory/licenseEnhancer.js +12 -0
  101. package/lib/inventory/mavenCentralMirror.js +534 -0
  102. package/lib/inventory/nodeBuildInfo.js +218 -0
  103. package/lib/inventory/nodeHostProbe.js +202 -0
  104. package/lib/inventory/osPackagePurl.js +244 -0
  105. package/lib/inventory/osPackageResolver.js +271 -87
  106. package/lib/inventory/osinfo.js +79 -31
  107. package/lib/inventory/osqueryTransform.js +114 -5
  108. package/lib/inventory/phpBuildInfo.js +438 -0
  109. package/lib/inventory/phpHostProbe.js +185 -0
  110. package/lib/inventory/rootfsCerts.js +426 -0
  111. package/lib/inventory/rubyBuildInfo.js +972 -0
  112. package/lib/inventory/rubyHostProbe.js +218 -0
  113. package/lib/inventory/scalaCoords.js +4 -3
  114. package/lib/inventory/scalaDedupe.js +440 -0
  115. package/lib/inventory/source.js +256 -5
  116. package/lib/inventory/spdx.js +144 -0
  117. package/lib/inventory/traceRunner.js +5 -1
  118. package/lib/inventory/tracedCrypto.js +129 -0
  119. package/lib/managers/binary.js +866 -186
  120. package/lib/managers/docker.js +245 -12
  121. package/lib/parsers/email.js +277 -0
  122. package/lib/parsers/largeJson.js +62 -13
  123. package/lib/parsers/vscodeExtensionId.js +183 -0
  124. package/lib/server/server.js +198 -2
  125. package/lib/stages/postgen/annotator.js +17 -1
  126. package/lib/stages/postgen/introspection/reflect.js +4 -1
  127. package/lib/stages/postgen/postgen.js +5 -5
  128. package/lib/stages/postgen/ruleEngine.js +71 -6
  129. package/lib/stages/postgen/specVersionCompat.js +30 -0
  130. package/lib/stages/pregen/envAudit.js +1 -0
  131. package/lib/stages/pregen/pregen.js +231 -62
  132. package/lib/validator/bomValidator.js +32 -6
  133. package/lib/validator/generated/runtime.mjs +13 -0
  134. package/lib/validator/generated/validate-1.6.mjs +27 -8
  135. package/lib/validator/generated/validate-1.7.mjs +32 -13
  136. package/lib/validator/generated/validate-2.0.mjs +31 -12
  137. package/lib/validator/index.js +1 -1
  138. package/package.json +19 -18
  139. package/types/bin/cdxgen-secure.d.ts +3 -0
  140. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  141. package/types/bin/repl.d.ts.map +1 -1
  142. package/types/lib/audit/index.d.ts.map +1 -1
  143. package/types/lib/cli/bomAssembly.d.ts +22 -3
  144. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  145. package/types/lib/cli/cliOptions.d.ts +43 -0
  146. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  147. package/types/lib/cli/index.d.ts.map +1 -1
  148. package/types/lib/cli/jsBom.d.ts +1 -17
  149. package/types/lib/cli/jsBom.d.ts.map +1 -1
  150. package/types/lib/cli/jvmBom.d.ts +20 -0
  151. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  152. package/types/lib/cli/managedBom.d.ts.map +1 -1
  153. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  154. package/types/lib/core/boundedFile.d.ts +70 -0
  155. package/types/lib/core/boundedFile.d.ts.map +1 -0
  156. package/types/lib/core/brotliProbe.d.ts +37 -0
  157. package/types/lib/core/brotliProbe.d.ts.map +1 -0
  158. package/types/lib/core/env.d.ts +22 -2
  159. package/types/lib/core/env.d.ts.map +1 -1
  160. package/types/lib/core/fs.d.ts.map +1 -1
  161. package/types/lib/core/httpClient.d.ts +24 -0
  162. package/types/lib/core/httpClient.d.ts.map +1 -1
  163. package/types/lib/core/lockPaths.d.ts +19 -0
  164. package/types/lib/core/lockPaths.d.ts.map +1 -0
  165. package/types/lib/core/processTree.d.ts +2 -0
  166. package/types/lib/core/processTree.d.ts.map +1 -1
  167. package/types/lib/core/projectConfig.d.ts.map +1 -1
  168. package/types/lib/core/projectTooling.d.ts +41 -0
  169. package/types/lib/core/projectTooling.d.ts.map +1 -0
  170. package/types/lib/core/propertySanitizer.d.ts +26 -0
  171. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  172. package/types/lib/core/toolRegistry.d.ts +36 -0
  173. package/types/lib/core/toolRegistry.d.ts.map +1 -0
  174. package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
  175. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  176. package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
  177. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
  178. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  179. package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
  180. package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
  181. package/types/lib/ecosystems/golem.d.ts +81 -1
  182. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  183. package/types/lib/ecosystems/gradleutils.d.ts +57 -21
  184. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  187. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  188. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  189. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-js.d.ts +17 -20
  191. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +14 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/pythonutils.d.ts +19 -1
  199. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  200. package/types/lib/ecosystems/rubyutils.d.ts +144 -12
  201. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  202. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  203. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  204. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  205. package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
  206. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
  207. package/types/lib/ecosystems/venvProbe.d.ts +40 -0
  208. package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
  209. package/types/lib/evinser/evinser.d.ts +88 -8
  210. package/types/lib/evinser/evinser.d.ts.map +1 -1
  211. package/types/lib/evinser/swiftsem.d.ts +10 -0
  212. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  213. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  214. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  215. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  216. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  217. package/types/lib/inventory/atomUtils.d.ts +33 -0
  218. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  219. package/types/lib/inventory/brotliDecoder.d.ts +58 -0
  220. package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
  221. package/types/lib/inventory/cacheDir.d.ts +12 -4
  222. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  223. package/types/lib/inventory/cbomutils.d.ts +17 -0
  224. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  225. package/types/lib/inventory/certificates.d.ts +205 -0
  226. package/types/lib/inventory/certificates.d.ts.map +1 -0
  227. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  228. package/types/lib/inventory/deps.d.ts +2 -1
  229. package/types/lib/inventory/deps.d.ts.map +1 -1
  230. package/types/lib/inventory/display.d.ts +18 -0
  231. package/types/lib/inventory/display.d.ts.map +1 -1
  232. package/types/lib/inventory/dosai.d.ts +162 -7
  233. package/types/lib/inventory/dosai.d.ts.map +1 -1
  234. package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
  235. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
  236. package/types/lib/inventory/dynamic.d.ts +6 -1
  237. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  238. package/types/lib/inventory/envcontext.d.ts +92 -8
  239. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  240. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  241. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  242. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  243. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  244. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  245. package/types/lib/inventory/gtfobins.d.ts.map +1 -1
  246. package/types/lib/inventory/hbom.d.ts.map +1 -1
  247. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  248. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  249. package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
  250. package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
  251. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  252. package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
  253. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  254. package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
  255. package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
  256. package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
  257. package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
  258. package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
  259. package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
  260. package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
  261. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  262. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  263. package/types/lib/inventory/osPackageResolver.d.ts +44 -2
  264. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  265. package/types/lib/inventory/osinfo.d.ts +31 -6
  266. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  267. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  268. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  269. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  270. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  271. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  272. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  273. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  274. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  275. package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
  276. package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
  277. package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
  278. package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
  279. package/types/lib/inventory/source.d.ts +38 -0
  280. package/types/lib/inventory/source.d.ts.map +1 -1
  281. package/types/lib/inventory/spdx.d.ts +15 -0
  282. package/types/lib/inventory/spdx.d.ts.map +1 -1
  283. package/types/lib/inventory/traceRunner.d.ts +4 -1
  284. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  285. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  286. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  287. package/types/lib/managers/binary.d.ts +10 -0
  288. package/types/lib/managers/binary.d.ts.map +1 -1
  289. package/types/lib/managers/docker.d.ts +24 -0
  290. package/types/lib/managers/docker.d.ts.map +1 -1
  291. package/types/lib/parsers/email.d.ts +19 -0
  292. package/types/lib/parsers/email.d.ts.map +1 -0
  293. package/types/lib/parsers/largeJson.d.ts +17 -3
  294. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  295. package/types/lib/server/server.d.ts +8 -0
  296. package/types/lib/server/server.d.ts.map +1 -1
  297. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  298. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  299. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  300. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  301. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  302. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  303. package/types/lib/stages/pregen/pregen.d.ts +26 -9
  304. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -1,13 +1,13 @@
1
1
  import { readFileSync, realpathSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
3
 
4
- import { build } from "@cdxgen/cdx-purl";
5
4
  import { globSync } from "glob";
6
5
 
7
6
  import { DEBUG_MODE, recordSymlinkResolution } from "../core/activity.js";
8
7
  import { safeExistsSync, safeSpawnSync } from "../core/fs.js";
9
8
  import { thoughtLog } from "../core/logger.js";
10
9
  import { getDistroInfo } from "./osinfo.js";
10
+ import { buildOsPackagePurl } from "./osPackagePurl.js";
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Caches
@@ -48,58 +48,34 @@ function parseAlpinePackage(pkgStr) {
48
48
  // ---------------------------------------------------------------------------
49
49
  // Build a PackageURL string from resolved package info.
50
50
  //
51
- // Distro qualifiers (distro, distro_name) are taken from /etc/os-release via
52
- // getDistroInfo() so they are always accurate, never hardcoded.
51
+ // deb, rpm, apk and Homebrew packages are named by buildOsPackagePurl, the
52
+ // function the OBOM rows use as well, so a file and the package that owns it
53
+ // share one identity with the package an OBOM or a container scan lists. The
54
+ // distro qualifiers come from /etc/os-release via getDistroInfo(), never from
55
+ // a hard-coded table.
53
56
  //
54
- // "brew" is not an official PackageURL type — Homebrew packages are emitted
55
- // as pkg:generic with a package_manager=homebrew qualifier.
57
+ // Homebrew has no official purl type yet, but cdx-purl builds pkg:brew, as the
58
+ // OBOM does for its homebrew_packages rows. A brew purl carries no qualifiers:
59
+ // this used to pass an `arch` that the generic type it fell back to does not
60
+ // allow, so build() threw and a file of a Homebrew package got no purl at all.
56
61
  // ---------------------------------------------------------------------------
57
62
  function buildPurl(pkgInfo) {
58
- let purlType = pkgInfo.type;
59
- let namespace;
60
- const qualifiers = {};
61
-
62
- if (pkgInfo.arch) {
63
- qualifiers.arch = pkgInfo.arch;
64
- }
65
-
66
- if (purlType === "deb" || purlType === "apk" || purlType === "rpm") {
67
- const di = getDistroInfo();
68
- namespace = di.namespace;
69
-
70
- // distro qualifier: ID-VERSION_ID (e.g. "fedora-25", "alpine-3.17")
71
- if (di.distroId) {
72
- qualifiers.distro = di.distroId;
73
- }
74
- // distro_name qualifier: codename (e.g. "jammy", "bookworm")
75
- if (di.distroName) {
76
- qualifiers.distro_name = di.distroName;
77
- }
78
- } else if (purlType === "brew") {
79
- // No official brew purl type — fall back to generic. This used to add a
80
- // `package_manager=homebrew` qualifier, which generic does not allow, so
81
- // build() threw and the catch below swallowed it: Homebrew packages silently
82
- // received no purl at all on macOS. The provenance is instead recorded as a
83
- // property by the caller.
84
- purlType = "generic";
85
- }
63
+ const distro =
64
+ pkgInfo.type === "brew" || !pkgInfo.type ? undefined : getDistroInfo();
65
+ return buildOsPackagePurl(pkgInfo, distro) || "";
66
+ }
86
67
 
87
- const finalQualifiers = Object.keys(qualifiers).length
88
- ? qualifiers
89
- : undefined;
68
+ // apk names an architecture its own way, which is not the way node does.
69
+ const APK_ARCHITECTURES = {
70
+ x64: "x86_64",
71
+ arm64: "aarch64",
72
+ ia32: "x86",
73
+ ppc64: "ppc64le",
74
+ loong64: "loongarch64",
75
+ };
90
76
 
91
- try {
92
- return build({
93
- type: purlType,
94
- namespace: namespace || undefined || null,
95
- name: pkgInfo.name,
96
- version: pkgInfo.version || undefined || null,
97
- qualifiers: finalQualifiers || null,
98
- subpath: undefined || null,
99
- });
100
- } catch (_e) {
101
- return "";
102
- }
77
+ function apkArchitecture() {
78
+ return APK_ARCHITECTURES[process.arch] || process.arch;
103
79
  }
104
80
 
105
81
  // ---------------------------------------------------------------------------
@@ -111,7 +87,7 @@ function buildPurl(pkgInfo) {
111
87
  * correctly computed purl with distro qualifiers derived from /etc/os-release.
112
88
  *
113
89
  * @param {string} filePath - Absolute path to the library file
114
- * @returns {{ name: string, version: string, arch: string, type: string, purl: string } | undefined}
90
+ * @returns {{ name: string, version: string, arch: string, type: string, epoch?: string, purl: string } | undefined}
115
91
  */
116
92
  export function resolvePackageForFile(filePath) {
117
93
  if (!filePath) {
@@ -190,27 +166,30 @@ function _resolveLinux(filePath) {
190
166
  return {
191
167
  name,
192
168
  version,
193
- arch: process.arch === "x64" ? "x86_64" : process.arch,
169
+ arch: apkArchitecture(),
194
170
  type: "apk",
195
171
  };
196
172
  }
197
173
  }
198
174
 
199
- // 3. RPM-based (rpm -qf)
175
+ // 3. RPM-based (rpm -qf). The epoch is part of an rpm's identity; %{EPOCH} is
176
+ // (none) for a package without one, so the conditional makes that a 0, which
177
+ // means the same.
200
178
  const rpmRes = safeSpawnSync("rpm", [
201
179
  "-qf",
202
180
  "--qf",
203
- "%{NAME} %{VERSION}-%{RELEASE} %{ARCH}\n",
181
+ "%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION}-%{RELEASE} %{ARCH}\n",
204
182
  filePath,
205
183
  ]);
206
184
  if (rpmRes && rpmRes.status === 0 && rpmRes.stdout) {
207
185
  const line = rpmRes.stdout.split("\n")[0].trim();
208
186
  const parts = line.split(" ");
209
- if (parts.length >= 3) {
187
+ if (parts.length >= 4) {
210
188
  return {
211
189
  name: parts[0],
212
- version: parts[1],
213
- arch: parts[2],
190
+ epoch: parts[1],
191
+ version: parts[2],
192
+ arch: parts[3],
214
193
  type: "rpm",
215
194
  };
216
195
  }
@@ -233,7 +212,7 @@ function _resolveDarwin(filePath) {
233
212
  name: segments[0],
234
213
  version: segments[1],
235
214
  arch: process.arch,
236
- type: "brew", // remapped to pkg:generic in buildPurl
215
+ type: "brew",
237
216
  };
238
217
  }
239
218
  }
@@ -379,6 +358,9 @@ function shouldPreferUsrMergedExecutablePath(file, existingFile) {
379
358
  * @param ldConf {String} Config file used by ldconfig to locate additional paths
380
359
  * @param ldConfDirPattern {String} Config directory that can contain more .conf files for ldconfig
381
360
  * @param excludePaths {Array[String]} Container-relative paths that should be excluded from the result set
361
+ * @param extraLibGlobs {Array[String]} Extra container-relative globs for files
362
+ * the default paths miss, such as libphp*.so under Apache's modules
363
+ * directory. The existing globs are unchanged; this adds candidates only.
382
364
  *
383
365
  * @return {Array[String]} List of executables
384
366
  */
@@ -388,6 +370,7 @@ export function collectSharedLibs(
388
370
  ldConf,
389
371
  ldConfDirPattern,
390
372
  excludePaths = [],
373
+ extraLibGlobs = [],
391
374
  ) {
392
375
  if (!libPaths) {
393
376
  return [];
@@ -399,6 +382,37 @@ export function collectSharedLibs(
399
382
  const ignoreList = [
400
383
  "**/*.{h,c,cpp,hpp,man,txt,md,htm,html,jar,ear,war,zip,tar,egg,keepme,gitignore,json,js,py,pyc}",
401
384
  ];
385
+ const addCandidate = (file) => {
386
+ let resolvedFile = file;
387
+ try {
388
+ resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
389
+ recordSymlinkResolution(join(basePath, file), resolvedFile, {
390
+ basePath,
391
+ metadata: {
392
+ resolutionKind: "shared-library",
393
+ },
394
+ reason: `Resolved shared library candidate ${file} to ${resolvedFile}.`,
395
+ });
396
+ } catch (err) {
397
+ recordSymlinkResolution(join(basePath, file), undefined, {
398
+ basePath,
399
+ errorCode: err?.code || err?.name,
400
+ metadata: {
401
+ resolutionKind: "shared-library",
402
+ },
403
+ reason: `Failed to resolve shared library candidate ${file}.`,
404
+ status: "failed",
405
+ });
406
+ // Broken symlinks or permission errors can prevent realpath resolution.
407
+ }
408
+ if (
409
+ excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
410
+ excludedPathSet.has(resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"))
411
+ ) {
412
+ return;
413
+ }
414
+ sharedLibs.push(file);
415
+ };
402
416
  const allLdConfDirs = ldConfDirPattern ? [ldConfDirPattern] : [];
403
417
  collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths);
404
418
  if (allLdConfDirs.length) {
@@ -428,37 +442,25 @@ export function collectSharedLibs(
428
442
  ignore: ignoreList,
429
443
  });
430
444
  for (const file of files) {
431
- let resolvedFile = file;
432
- try {
433
- resolvedFile = relative(basePath, realpathSync(join(basePath, file)));
434
- recordSymlinkResolution(join(basePath, file), resolvedFile, {
435
- basePath,
436
- metadata: {
437
- resolutionKind: "shared-library",
438
- },
439
- reason: `Resolved shared library candidate ${file} to ${resolvedFile}.`,
440
- });
441
- } catch (err) {
442
- recordSymlinkResolution(join(basePath, file), undefined, {
443
- basePath,
444
- errorCode: err?.code || err?.name,
445
- metadata: {
446
- resolutionKind: "shared-library",
447
- },
448
- reason: `Failed to resolve shared library candidate ${file}.`,
449
- status: "failed",
450
- });
451
- // Broken symlinks or permission errors can prevent realpath resolution.
452
- }
453
- if (
454
- excludedPathSet.has(file.replace(/^\/+/, "").replace(/\\/g, "/")) ||
455
- excludedPathSet.has(
456
- resolvedFile.replace(/^\/+/, "").replace(/\\/g, "/"),
457
- )
458
- ) {
459
- continue;
460
- }
461
- sharedLibs.push(file);
445
+ addCandidate(file);
446
+ }
447
+ } catch (_err) {
448
+ // ignore
449
+ }
450
+ }
451
+ for (const pattern of extraLibGlobs) {
452
+ try {
453
+ const files = globSync(pattern, {
454
+ cwd: basePath,
455
+ absolute: false,
456
+ nocase: true,
457
+ nodir: true,
458
+ dot: true,
459
+ follow: true,
460
+ ignore: ignoreList,
461
+ });
462
+ for (const file of files) {
463
+ addCandidate(file);
462
464
  }
463
465
  } catch (_err) {
464
466
  // ignore
@@ -491,3 +493,185 @@ function collectAllLdConfs(basePath, ldConf, allLdConfDirs, libPaths) {
491
493
  }
492
494
  }
493
495
  }
496
+
497
+ // Native modules that language package managers install outside the library
498
+ // directories the loader searches: CPython and PyPy extension modules, PHP
499
+ // extensions, Ruby gem extensions, Node.js addons, and the native half of a
500
+ // .NET runtime. A Brotli decoder compiled into one of them is invisible unless
501
+ // the file is read.
502
+ const NATIVE_EXTENSION_GLOBS = [
503
+ "**/site-packages/**/*.{so,pyd}",
504
+ "**/dist-packages/**/*.so",
505
+ "**/lib/php/**/*.so",
506
+ "**/lib/php*/modules/*.so",
507
+ "**/php*/ext/php_*.dll",
508
+ "**/gems/*/lib/**/*.{so,bundle}",
509
+ "**/gems/*/extensions/**/*.{so,bundle}",
510
+ "**/bundle/extensions/**/*.{so,bundle}",
511
+ "**/node_modules/**/*.node",
512
+ "**/dotnet/shared/*/*/*.so",
513
+ "**/.dotnet/shared/*/*/*.so",
514
+ // Of a Windows runtime's many DLLs, only this one is native and holds the
515
+ // decoder.
516
+ "**/dotnet/shared/*/*/System.IO.Compression.Native.dll",
517
+ ];
518
+ const NATIVE_EXTENSION_IGNORES = ["proc/**", "sys/**", "dev/**"];
519
+
520
+ /**
521
+ * The most native extension candidates one scan returns.
522
+ *
523
+ * @type {number}
524
+ */
525
+ export const MAX_NATIVE_EXTENSIONS = 4096;
526
+
527
+ /**
528
+ * Collect native extension modules under a root file system that the library
529
+ * collector does not see, leaving out paths a package database owns and paths
530
+ * already collected. Symlinks are not followed, so a link pointing out of the
531
+ * root is never listed.
532
+ *
533
+ * @param {string} basePath extracted root file system
534
+ * @param {string[]} [excludePaths] root-relative paths to leave out
535
+ * @returns {string[]} root-relative paths, sorted, capped at MAX_NATIVE_EXTENSIONS
536
+ */
537
+ export function collectNativeExtensions(basePath, excludePaths = []) {
538
+ if (!basePath) {
539
+ return [];
540
+ }
541
+ const excluded = new Set(
542
+ (excludePaths || []).map((f) => f.replace(/^\/+/, "").replace(/\\/g, "/")),
543
+ );
544
+ let files = [];
545
+ try {
546
+ files = globSync(NATIVE_EXTENSION_GLOBS, {
547
+ cwd: basePath,
548
+ absolute: false,
549
+ nodir: true,
550
+ dot: true,
551
+ follow: false,
552
+ ignore: NATIVE_EXTENSION_IGNORES,
553
+ });
554
+ } catch (_err) {
555
+ return [];
556
+ }
557
+ const result = new Set();
558
+ for (const file of files) {
559
+ const normalized = file.replace(/\\/g, "/").replace(/^\/+/, "");
560
+ if (!excluded.has(normalized)) {
561
+ result.add(normalized);
562
+ }
563
+ }
564
+ return Array.from(result).sort().slice(0, MAX_NATIVE_EXTENSIONS);
565
+ }
566
+
567
+ const OS_PACKAGE_PURL_PREFIXES = [
568
+ "pkg:deb/",
569
+ "pkg:apk/",
570
+ "pkg:rpm/",
571
+ "pkg:alpm/",
572
+ "pkg:qpkg/",
573
+ "pkg:generic/",
574
+ ];
575
+
576
+ /**
577
+ * Whether a source file is the installation record of a language package,
578
+ * the file a package manager writes when it installs the package: dist-info
579
+ * or egg-info metadata, an installed gemspec, a package.json inside
580
+ * node_modules, or a jar. A lock file or manifest only names packages, so an
581
+ * OS package owning one says nothing about who installed them.
582
+ *
583
+ * @param {string} filePath root-relative path with forward slashes
584
+ * @returns {boolean}
585
+ */
586
+ function isInstallRecord(filePath) {
587
+ const parts = filePath.split("/");
588
+ const name = parts.at(-1) || "";
589
+ const parent = parts.at(-2) || "";
590
+ if (parent.endsWith(".dist-info") || parent.endsWith(".egg-info")) {
591
+ return true;
592
+ }
593
+ if (name.endsWith(".egg-info") || name.endsWith(".jar")) {
594
+ return true;
595
+ }
596
+ if (name.endsWith(".gemspec") && parts.includes("specifications")) {
597
+ return true;
598
+ }
599
+ return name === "package.json" && parts.includes("node_modules");
600
+ }
601
+
602
+ /**
603
+ * Mark language packages that an OS package installed. Alpine's py3-brotli
604
+ * ships a Brotli-1.1.0.dist-info that a Python scan reports as
605
+ * pkg:pypi/brotli@1.1.0, but a distro fixes it by backporting under the same
606
+ * upstream version, so a PyPI advisory range keeps matching after the fix. The
607
+ * `cdx:os:installedBy` property names the OS package that owns the
608
+ * installation record, so a consumer can judge the component by that package
609
+ * instead.
610
+ *
611
+ * A component is marked only when every installation record it was read from
612
+ * belongs to an OS package: a copy that pip put in a virtual environment
613
+ * beside the distro's is not the distro's to fix. Run it after duplicates
614
+ * merge, so that a merged component carries all of its records.
615
+ *
616
+ * @param {Object[]} components BOM components, mutated in place
617
+ * @param {Map<string, string>|undefined} ownerByPath root-relative file path to
618
+ * owning OS package bom-ref
619
+ * @param {string} [rootDir] extracted root, stripped from absolute source paths
620
+ * @returns {number} how many components were marked
621
+ */
622
+ export function markOsInstalledLanguagePackages(
623
+ components,
624
+ ownerByPath,
625
+ rootDir,
626
+ ) {
627
+ if (!ownerByPath?.size || !Array.isArray(components)) {
628
+ return 0;
629
+ }
630
+ const rootPrefix = rootDir
631
+ ? rootDir.replaceAll("\\", "/").replace(/\/+$/, "")
632
+ : undefined;
633
+ let marked = 0;
634
+ for (const component of components) {
635
+ const purl = component?.purl;
636
+ if (
637
+ typeof purl !== "string" ||
638
+ !purl.startsWith("pkg:") ||
639
+ OS_PACKAGE_PURL_PREFIXES.some((prefix) => purl.startsWith(prefix)) ||
640
+ !Array.isArray(component.properties) ||
641
+ component.properties.some((prop) => prop.name === "cdx:os:installedBy")
642
+ ) {
643
+ continue;
644
+ }
645
+ const owners = new Set();
646
+ let records = 0;
647
+ let unowned = 0;
648
+ for (const prop of component.properties) {
649
+ if (prop.name !== "internal:SrcFile" || typeof prop.value !== "string") {
650
+ continue;
651
+ }
652
+ let filePath = prop.value.replaceAll("\\", "/");
653
+ if (rootPrefix && filePath.startsWith(`${rootPrefix}/`)) {
654
+ filePath = filePath.slice(rootPrefix.length);
655
+ }
656
+ filePath = filePath.replace(/^\/+/, "");
657
+ if (!isInstallRecord(filePath)) {
658
+ continue;
659
+ }
660
+ records += 1;
661
+ const owner = ownerByPath.get(filePath);
662
+ if (owner) {
663
+ owners.add(owner);
664
+ } else {
665
+ unowned += 1;
666
+ }
667
+ }
668
+ if (!records || unowned) {
669
+ continue;
670
+ }
671
+ for (const owner of Array.from(owners).sort()) {
672
+ component.properties.push({ name: "cdx:os:installedBy", value: owner });
673
+ }
674
+ marked += 1;
675
+ }
676
+ return marked;
677
+ }
@@ -49,14 +49,34 @@ export const OS_DISTRO_ALIAS = {
49
49
  "ubuntu-24.10": "oracular",
50
50
  "ubuntu-25.04": "plucky",
51
51
  "ubuntu-25.10": "questing",
52
+ "ubuntu-26.04": "resolute",
53
+ "ubuntu-26.10": "stonking",
52
54
  "debian-15": "duke",
53
55
  "debian-14": "forky",
54
56
  "debian-14.5": "forky",
55
57
  "debian-13": "trixie",
58
+ "debian-13.7": "trixie",
59
+ "debian-13.6": "trixie",
56
60
  "debian-13.5": "trixie",
61
+ "debian-13.4": "trixie",
62
+ "debian-13.3": "trixie",
63
+ "debian-13.2": "trixie",
64
+ "debian-13.1": "trixie",
57
65
  "debian-12": "bookworm",
58
- "debian-12.5": "bookworm",
66
+ "debian-12.15": "bookworm",
67
+ "debian-12.14": "bookworm",
68
+ "debian-12.13": "bookworm",
69
+ "debian-12.12": "bookworm",
70
+ "debian-12.11": "bookworm",
71
+ "debian-12.10": "bookworm",
72
+ "debian-12.9": "bookworm",
73
+ "debian-12.8": "bookworm",
74
+ "debian-12.7": "bookworm",
59
75
  "debian-12.6": "bookworm",
76
+ "debian-12.5": "bookworm",
77
+ "debian-12.4": "bookworm",
78
+ "debian-12.2": "bookworm",
79
+ "debian-12.1": "bookworm",
60
80
  "debian-11": "bullseye",
61
81
  "debian-11.5": "bullseye",
62
82
  "debian-10": "buster",
@@ -256,15 +276,20 @@ export function _resetOsReleaseCache() {
256
276
  * Derive structured distro information from an os-release file.
257
277
  *
258
278
  * Returns an object with:
259
- * - purlType {string} "deb" | "apk" | "rpm"
279
+ * - purlType {string} "deb" | "apk" | "rpm" | "alpm" (Arch and the
280
+ * distros built on it)
260
281
  * - namespace {string} canonical purl namespace (e.g. "ubuntu", "alpine",
261
282
  * "azure-linux" for both Azure Linux and CBL-Mariner)
262
283
  * - distroId {string} ID + "-" + VERSION_ID with a canonical vendor
263
- * prefix (e.g. "ubuntu-22.04", "azure-linux-3.0")
264
- * - distroName {string} codename/alias (e.g. "jammy")
284
+ * prefix (e.g. "ubuntu-22.04", "azure-linux-3.0",
285
+ * "alpine-3.24.3")
286
+ * - distroName {string} release codename, or a token OS_DISTRO_ALIAS knows
287
+ * (e.g. "jammy", "rhel", "alpine-3.24"); empty when
288
+ * the distro has neither
265
289
  *
266
- * Mirrors the logic in lib/managers/binary.js getOSPackages() so that both
267
- * callers share a single implementation.
290
+ * lib/managers/binary.js getOSPackages() reads its distro from here, so a
291
+ * container scan and an OBOM run on the same system give a package the same
292
+ * `distro` and `distro_name` qualifiers.
268
293
  *
269
294
  * @param {string} [root="/"] - Filesystem root to look for os-release.
270
295
  * @returns {{ purlType: string, namespace: string, distroId: string, distroName: string }}
@@ -290,6 +315,8 @@ export function getDistroInfo(root = "/") {
290
315
  case "alpine":
291
316
  case "alpaquita":
292
317
  case "openwrt":
318
+ case "wolfi":
319
+ case "chainguard":
293
320
  purlType = "apk";
294
321
  break;
295
322
  case "sles":
@@ -312,11 +339,23 @@ export function getDistroInfo(root = "/") {
312
339
  case "photon":
313
340
  purlType = "rpm";
314
341
  break;
342
+ case "arch":
343
+ case "archarm":
344
+ case "manjaro":
345
+ case "endeavouros":
346
+ case "cachyos":
347
+ case "steamos":
348
+ case "artix":
349
+ purlType = "alpm";
350
+ break;
315
351
  default:
316
352
  if (idLike.includes("debian") || idLike.includes("ubuntu")) {
317
353
  purlType = "deb";
318
354
  } else if (idLike.includes("alpine")) {
319
355
  purlType = "apk";
356
+ } else if (idLike.split(/\s+/u).includes("arch")) {
357
+ // A token, not a substring: ID_LIKE is a space-separated list.
358
+ purlType = "alpm";
320
359
  } else if (
321
360
  idLike.includes("rhel") ||
322
361
  idLike.includes("centos") ||
@@ -343,41 +382,50 @@ export function getDistroInfo(root = "/") {
343
382
  namespace = "debian";
344
383
  } else if (idLike.includes("ubuntu")) {
345
384
  namespace = "ubuntu";
385
+ } else if (purlType === "alpm") {
386
+ namespace = "arch";
346
387
  } else {
347
388
  namespace = "linux";
348
389
  }
349
390
  }
350
391
 
351
- // Build distroId = ID-VERSION_ID (e.g. "fedora-25", "alpine-3.17"),
352
- // canonicalising the vendor prefix so one release has a single qualifier
353
- // value (e.g. "mariner-2.0" and "azurelinux-3.0" -> "azure-linux-2.0"/
354
- // "azure-linux-3.0")
355
- let distroId = versionId ? `${rawId}-${versionId}` : rawId;
356
- distroId = canonicalDistroQualifier(distroId);
392
+ // Build distroId = ID-VERSION_ID as os-release writes it (e.g. "fedora-25",
393
+ // "alpine-3.24.3"), canonicalising the vendor prefix so one release has a
394
+ // single qualifier value (e.g. "mariner-2.0" and "azurelinux-3.0" ->
395
+ // "azure-linux-2.0"/"azure-linux-3.0")
396
+ const releaseId = versionId ? `${rawId}-${versionId}` : rawId;
397
+ const distroId = canonicalDistroQualifier(releaseId);
357
398
 
358
- // Special-case Alpine: truncate to major.minor
359
- if (purlType === "apk" && versionId) {
399
+ // distro_name is a release codename or a token OS_DISTRO_ALIAS knows
400
+ // ("bookworm", "jammy", "rhel", "alpine-3.24"), never a product display
401
+ // name: "AlmaLinux" or "Rocky Linux 9" has no place in a purl, since RPM
402
+ // distros track packages flat and a display name there breaks vulnerability
403
+ // matching. A product name counts only when it resolves through the alias
404
+ // table, which holds the RHEL names.
405
+ let distroName = (info.VERSION_CODENAME || "").toLowerCase();
406
+ if (!distroName) {
407
+ const productName = (
408
+ info.CENTOS_MANTISBT_PROJECT ||
409
+ info.REDHAT_BUGZILLA_PRODUCT ||
410
+ info.REDHAT_SUPPORT_PRODUCT ||
411
+ ""
412
+ ).toLowerCase();
413
+ if (productName && Object.hasOwn(OS_DISTRO_ALIAS, productName)) {
414
+ distroName = OS_DISTRO_ALIAS[productName];
415
+ }
416
+ }
417
+ // Alpine names its release series: 3.24.3 belongs to alpine-3.24, the unit
418
+ // its security data is kept in.
419
+ if (rawId === "alpine" && versionId) {
360
420
  const parts = versionId.split(".");
361
421
  if (parts.length >= 2) {
362
- distroId = `${rawId}-${parts[0]}.${parts[1]}`;
422
+ distroName = `alpine-${parts[0]}.${parts[1]}`;
363
423
  }
364
424
  }
365
-
366
- // Determine codename / alias
367
- let distroName =
368
- info.VERSION_CODENAME ||
369
- info.CENTOS_MANTISBT_PROJECT ||
370
- info.REDHAT_BUGZILLA_PRODUCT ||
371
- info.REDHAT_SUPPORT_PRODUCT ||
372
- "";
373
- distroName = distroName.toLowerCase();
374
-
375
- // Resolve well-known display name aliases
376
- if (distroName.includes(" ") && OS_DISTRO_ALIAS[distroName]) {
377
- distroName = OS_DISTRO_ALIAS[distroName];
378
- }
379
- if (!distroName && OS_DISTRO_ALIAS[distroId]) {
380
- distroName = OS_DISTRO_ALIAS[distroId];
425
+ // The alias of the release itself (ubuntu-24.04 -> noble) wins over the
426
+ // codename the file carries.
427
+ if (versionId && Object.hasOwn(OS_DISTRO_ALIAS, releaseId)) {
428
+ distroName = OS_DISTRO_ALIAS[releaseId];
381
429
  }
382
430
 
383
431
  return { purlType, namespace, distroId, distroName };