@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
@@ -6,7 +6,15 @@ import {
6
6
  statSync,
7
7
  } from "node:fs";
8
8
  import { platform as _platform, homedir } from "node:os";
9
- import { basename, dirname, extname, join, relative, resolve } from "node:path";
9
+ import {
10
+ basename,
11
+ dirname,
12
+ extname,
13
+ isAbsolute,
14
+ join,
15
+ relative,
16
+ resolve,
17
+ } from "node:path";
10
18
  import process from "node:process";
11
19
 
12
20
  import { build, Purl } from "@cdxgen/cdx-purl";
@@ -30,26 +38,62 @@ import {
30
38
  safeRmSync,
31
39
  safeSpawnSync,
32
40
  } from "../core/fs.js";
41
+ import { thoughtLog } from "../core/logger.js";
33
42
  import { mapWithConcurrency } from "../core/parallel.js";
34
43
  import { isValidDriveRoot } from "../core/paths.js";
35
44
  import { isCycloneDxComponentTypeEnabled } from "../inventory/bomUtils.js";
45
+ import {
46
+ attachBrotliDecoder,
47
+ createBrotliInventory,
48
+ finishBrotliInventory,
49
+ MAX_BROTLI_PROBE_BYTES,
50
+ MAX_BROTLI_PROBES,
51
+ readBrotliDecoder,
52
+ } from "../inventory/brotliDecoder.js";
53
+ import {
54
+ CertificateCollector,
55
+ classifyCryptoFile,
56
+ } from "../inventory/certificates.js";
36
57
  import { createContainerRiskProperties } from "../inventory/containerRisk.js";
58
+ import { collectDotnetRuntimes } from "../inventory/dotnetRuntime.js";
37
59
  import {
38
60
  attachIdentityTools,
39
61
  extractToolRefs,
40
62
  } from "../inventory/evidenceUtils.js";
63
+ import {
64
+ PRE_1_18_VERSION,
65
+ readGoBuildVersion,
66
+ } from "../inventory/goBuildInfo.js";
41
67
  import { createGtfoBinsProperties } from "../inventory/gtfobins.js";
68
+ import {
69
+ attachNodeBuild,
70
+ createNodeRuntimeInventory,
71
+ isNodeBinaryName,
72
+ MAX_NODE_PROBES,
73
+ readNodeBuildVersion,
74
+ } from "../inventory/nodeBuildInfo.js";
42
75
  import {
43
76
  canonicalDistroQualifier,
77
+ getDistroInfo,
44
78
  isCleanDistroQualifier,
45
79
  OS_DISTRO_ALIAS,
46
80
  OS_NAMESPACE_ALIAS,
47
81
  readOsRelease,
48
82
  } from "../inventory/osinfo.js";
83
+ import { isRealArchitecture } from "../inventory/osPackagePurl.js";
49
84
  import {
50
85
  collectExecutables,
86
+ collectNativeExtensions,
51
87
  collectSharedLibs,
52
88
  } from "../inventory/osPackageResolver.js";
89
+ import {
90
+ attachPhpBuild,
91
+ classifyPhpSapi,
92
+ createPhpRuntimeInventory,
93
+ MAX_PHP_PROBES,
94
+ packageStoreOf,
95
+ readPhpBuildVersion,
96
+ } from "../inventory/phpBuildInfo.js";
53
97
  import {
54
98
  resolveCdxgenPlugins,
55
99
  resolvePluginBinary,
@@ -60,11 +104,22 @@ import {
60
104
  parsePurlLoose,
61
105
  tryBuildPurl,
62
106
  } from "../inventory/purl.js";
107
+ import { collectRootfsCertificates } from "../inventory/rootfsCerts.js";
108
+ import {
109
+ attachRubyBuild,
110
+ createRubyRuntimeInventory,
111
+ deriveOsPackagedRubyRuntimes,
112
+ isLibrubyName,
113
+ isRubyBinaryName,
114
+ MAX_RUBY_PROBES,
115
+ readRubyBuildVersion,
116
+ } from "../inventory/rubyBuildInfo.js";
63
117
  import {
64
118
  adjustLicenseInformation,
65
119
  findLicenseId,
66
120
  isSpdxLicenseExpression,
67
121
  } from "../inventory/spdx.js";
122
+ import { isValidIdnEmail } from "../parsers/email.js";
68
123
  import { getDirs } from "./containerutils.js";
69
124
 
70
125
  const isWin = _platform() === "win32";
@@ -510,6 +565,15 @@ const COMMON_RUNTIMES = [
510
565
  "openjdk25-jre",
511
566
  ];
512
567
 
568
+ // Runtime packages whose name carries the series, such as Debian's ruby3.1
569
+ // and libruby3.1, openSUSE's libruby3_4-3_4 and Wolfi's ruby-3.3.
570
+ const VERSIONED_RUNTIME_PATTERNS = [
571
+ /^(lib)?ruby\d+\.\d+(t64)?$/,
572
+ /^ruby-\d+\.\d+$/,
573
+ /^ruby-libs$/,
574
+ /^libruby\d+_\d+-\d+_\d+$/,
575
+ ];
576
+
513
577
  /**
514
578
  * Run the `cargo-auditable` companion binary against the given source path and
515
579
  * return its stdout.
@@ -654,7 +718,35 @@ export function canonicaliseOsPackageNamespace(
654
718
  group,
655
719
  distroId,
656
720
  ) {
657
- if (!purlObj || !OS_PURL_TYPES.includes(purlObj.type)) {
721
+ if (!purlObj) {
722
+ return group;
723
+ }
724
+ // trivy invents a `bottlerocket` purl type for Bottlerocket host packages
725
+ // (trivy pkg/purl/purl.go, packageurlTypeBottlerocket); the purl spec has
726
+ // no such type — @cdxgen/cdx-purl has no generated type class for it, and
727
+ // no vulnerability feed stores under it. Rewrite to the form cdxgen's own
728
+ // os-release path produces for a Bottlerocket host: type rpm, namespace
729
+ // bottlerocket (the os-release ID; OS_NAMESPACE_ALIAS needs no entry).
730
+ // The distro qualifier trivy stamps (bottlerocket-<os version>) survives
731
+ // unchanged. The caller's group is returned as for any other rpm distro
732
+ // without an alias, so getOSPackages still records the rpm type and the
733
+ // distro in allTypes (oci:image:componentTypes).
734
+ if (purlObj.type === "bottlerocket") {
735
+ purlObj.type = "rpm";
736
+ purlObj.namespace = "bottlerocket";
737
+ comp.group = "bottlerocket";
738
+ comp.purl = build({
739
+ type: purlObj.type,
740
+ namespace: purlObj.namespace,
741
+ name: name,
742
+ version: purlObj.version || null,
743
+ qualifiers: purlObj.qualifiers || null,
744
+ subpath: purlObj.subpath || null,
745
+ });
746
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
747
+ return group;
748
+ }
749
+ if (!OS_PURL_TYPES.includes(purlObj.type)) {
658
750
  return group;
659
751
  }
660
752
  const os_ns = purlObj.namespace || group || "";
@@ -773,7 +865,7 @@ export function dropRedundantSourceComponents(pkgList) {
773
865
  const installedPurls = new Set();
774
866
  for (const comp of pkgList) {
775
867
  if (comp?.purl && !comp?.tags?.includes("source")) {
776
- installedPurls.add(comp.purl);
868
+ installedPurls.add(withoutArch(comp.purl));
777
869
  }
778
870
  }
779
871
  if (!installedPurls.size) {
@@ -781,13 +873,33 @@ export function dropRedundantSourceComponents(pkgList) {
781
873
  }
782
874
  for (let i = pkgList.length - 1; i >= 0; i--) {
783
875
  const comp = pkgList[i];
784
- if (comp?.tags?.includes("source") && installedPurls.has(comp.purl)) {
876
+ if (
877
+ comp?.tags?.includes("source") &&
878
+ installedPurls.has(withoutArch(comp.purl))
879
+ ) {
785
880
  pkgList.splice(i, 1);
786
881
  }
787
882
  }
788
883
  return pkgList;
789
884
  }
790
885
 
886
+ // The arch qualifier deb and rpm purls give a source package.
887
+ const SOURCE_PACKAGE_ARCH = { deb: "source", rpm: "src" };
888
+
889
+ // A source shadow is named like an installed package of the same name and
890
+ // version, whatever arch either names, so arch is left out of the comparison.
891
+ function withoutArch(purl) {
892
+ const [base, query] = `${purl}`.split("?");
893
+ if (!query) {
894
+ return base;
895
+ }
896
+ const kept = query
897
+ .split("&")
898
+ .filter((pair) => !pair.startsWith("arch="))
899
+ .sort();
900
+ return kept.length ? `${base}?${kept.join("&")}` : base;
901
+ }
902
+
791
903
  /**
792
904
  * Get the packages installed in the container image filesystem.
793
905
  *
@@ -923,80 +1035,20 @@ export async function getOSPackages(src, imageConfig, options = {}) {
923
1035
  if (DEBUG_MODE) {
924
1036
  console.log(osReleaseData);
925
1037
  }
926
- // distro_name must be a real release codename/token (bookworm, jammy,
927
- // rhel-9, alpine-3.19), never a distro *display name*. VERSION_CODENAME is
928
- // authoritative when present; otherwise the RHEL/CentOS product-name
929
- // fallbacks are only accepted when they resolve through OS_DISTRO_ALIAS
930
- // (e.g. "red hat enterprise linux 9" -> "rhel-9"). Display names such as
931
- // "Rocky Linux" or "AlmaLinux" have no alias entry and are dropped so we
932
- // never emit a bogus distro_name qualifier (RPM distros track packages
933
- // flat, without a release segment, so folding a display name breaks
934
- // downstream vuln-db matching).
935
- let distro_codename = (
936
- osReleaseData["VERSION_CODENAME"] || ""
937
- ).toLowerCase();
938
- if (!distro_codename) {
939
- const distro_display_name = (
940
- osReleaseData["CENTOS_MANTISBT_PROJECT"] ||
941
- osReleaseData["REDHAT_BUGZILLA_PRODUCT"] ||
942
- osReleaseData["REDHAT_SUPPORT_PRODUCT"] ||
943
- ""
944
- ).toLowerCase();
945
- if (distro_display_name && OS_DISTRO_ALIAS[distro_display_name]) {
946
- distro_codename = OS_DISTRO_ALIAS[distro_display_name];
947
- }
948
- }
949
- let distro_id = osReleaseData["ID"] || "";
950
- const distro_id_like = osReleaseData["ID_LIKE"] || "";
951
- let purl_type = "rpm";
952
- switch (distro_id) {
953
- case "debian":
954
- case "ubuntu":
955
- case "pop":
956
- purl_type = "deb";
957
- break;
958
- case "sles":
959
- case "suse":
960
- case "opensuse":
961
- purl_type = "rpm";
962
- break;
963
- case "alpine":
964
- purl_type = "apk";
965
- if (osReleaseData.VERSION_ID) {
966
- const versionParts = osReleaseData["VERSION_ID"].split(".");
967
- if (versionParts.length >= 2) {
968
- distro_codename = `alpine-${versionParts[0]}.${versionParts[1]}`;
969
- }
970
- }
971
- break;
972
- case "alpaquita":
973
- purl_type = "apk";
974
- break;
975
- default:
976
- if (distro_id_like.includes("debian")) {
977
- purl_type = "deb";
978
- } else if (distro_id_like.includes("alpine")) {
979
- purl_type = "apk";
980
- } else if (
981
- distro_id_like.includes("rhel") ||
982
- distro_id_like.includes("centos") ||
983
- distro_id_like.includes("fedora")
984
- ) {
985
- purl_type = "rpm";
986
- }
987
- break;
988
- }
989
- if (osReleaseData["VERSION_ID"]) {
990
- distro_id = `${distro_id}-${osReleaseData["VERSION_ID"]}`;
991
- if (OS_DISTRO_ALIAS[distro_id]) {
992
- distro_codename = OS_DISTRO_ALIAS[distro_id];
993
- }
994
- }
995
- // Canonicalise the distro qualifier vendor (e.g. "mariner-2.0" ->
996
- // "azure-linux-2.0") so every spelling of one release yields a single
997
- // qualifier value. This runs before distro_id is used to overwrite
998
- // trivy's own qualifier below.
999
- distro_id = canonicalDistroQualifier(distro_id);
1038
+ // The distro of the rootfs is decided in getDistroInfo, which an OBOM run
1039
+ // on the same system reads too, so a package gets the same qualifiers
1040
+ // from either scan. distro_name is a real release codename/token
1041
+ // (bookworm, jammy, rhel, alpine-3.24), never a distro *display name*:
1042
+ // "Rocky Linux" or "AlmaLinux" is dropped, since RPM distros track
1043
+ // packages flat and a display name there breaks downstream vuln-db
1044
+ // matching. distro is ID-VERSION_ID with the vendor canonicalised (e.g.
1045
+ // "mariner-2.0" -> "azure-linux-2.0"), so every spelling of one release
1046
+ // yields a single qualifier value; it overwrites trivy's own qualifier
1047
+ // below.
1048
+ const distroInfo = getDistroInfo(src);
1049
+ let distro_codename = distroInfo.distroName;
1050
+ const distro_id = distroInfo.distroId;
1051
+ const purl_type = distroInfo.purlType;
1000
1052
  const tmpDependencies = {};
1001
1053
  tools = enrichToolComponents(
1002
1054
  (Array.isArray(tmpBom?.metadata?.tools)
@@ -1030,6 +1082,27 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1030
1082
  try {
1031
1083
  purlObj = Purl.parse(comp.purl);
1032
1084
  purlObj.qualifiers = purlObj.qualifiers || {};
1085
+ if (
1086
+ OS_PURL_TYPES.includes(purlObj.type) &&
1087
+ "arch" in purlObj.qualifiers &&
1088
+ !isRealArchitecture(purlObj.qualifiers.arch)
1089
+ ) {
1090
+ // trivy writes arch=None for a package with no architecture,
1091
+ // such as an rpm's gpg-pubkey; a package has the architecture
1092
+ // it has, or none, as in an OBOM.
1093
+ delete purlObj.qualifiers.arch;
1094
+ comp.purl = build({
1095
+ type: purlObj.type,
1096
+ namespace: purlObj.namespace || null,
1097
+ name: name,
1098
+ version: purlObj.version || null,
1099
+ qualifiers: Object.keys(purlObj.qualifiers).length
1100
+ ? purlObj.qualifiers
1101
+ : null,
1102
+ subpath: purlObj.subpath || null,
1103
+ });
1104
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
1105
+ }
1033
1106
  } catch (_err) {
1034
1107
  // continue regardless of error
1035
1108
  }
@@ -1115,16 +1188,20 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1115
1188
  if (distro_codename !== "") {
1116
1189
  allTypes.add(distro_codename);
1117
1190
  allTypes.add(purlObj.namespace);
1118
- comp.purl = build({
1119
- type: purlObj.type,
1120
- namespace: purlObj.namespace || null,
1121
- name: name,
1122
- version: purlObj.version || null,
1123
- qualifiers: purlObj.qualifiers || null,
1124
- subpath: purlObj.subpath || null,
1125
- });
1126
- comp["bom-ref"] = decodeURIComponent(comp.purl);
1127
1191
  }
1192
+ // Rebuilt whether or not the distro has a codename: the distro
1193
+ // stamped above is ID-VERSION_ID for every distro, and a purl
1194
+ // left as trivy wrote it keeps trivy's own spelling (a bare
1195
+ // 20230201 for a Wolfi image).
1196
+ comp.purl = build({
1197
+ type: purlObj.type,
1198
+ namespace: purlObj.namespace || null,
1199
+ name: name,
1200
+ version: purlObj.version || null,
1201
+ qualifiers: purlObj.qualifiers || null,
1202
+ subpath: purlObj.subpath || null,
1203
+ });
1204
+ comp["bom-ref"] = decodeURIComponent(comp.purl);
1128
1205
  } catch (_err) {
1129
1206
  // continue regardless of error
1130
1207
  }
@@ -1325,6 +1402,14 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1325
1402
  if (epoch) {
1326
1403
  newCompQualifiers.epoch = epoch;
1327
1404
  }
1405
+ // A source package is built for no architecture. The deb and
1406
+ // rpm purl types name it `source` and `src`; copying the arch
1407
+ // of whichever binary led here (`all` from a -doc package)
1408
+ // misnames it.
1409
+ const sourceArch = SOURCE_PACKAGE_ARCH[purlObj.type];
1410
+ if (sourceArch && newCompQualifiers.arch) {
1411
+ newCompQualifiers.arch = sourceArch;
1412
+ }
1328
1413
  newComp.purl = build({
1329
1414
  type: purlObj.type,
1330
1415
  namespace: purlObj.namespace || null,
@@ -1360,6 +1445,7 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1360
1445
  components: ownedFileComponents,
1361
1446
  dependenciesList: ownedFileDependencies,
1362
1447
  ownedFilePaths,
1448
+ ownerByPath,
1363
1449
  services: ownedServices,
1364
1450
  } = await createOSPackageFileComponents(src, osPackageEntries);
1365
1451
  if (ownedFileComponents.length) {
@@ -1404,12 +1490,91 @@ export async function getOSPackages(src, imageConfig, options = {}) {
1404
1490
  "/etc/ld.so.conf",
1405
1491
  "/etc/ld.so.conf.d/*.conf",
1406
1492
  ownedFilePaths,
1493
+ // Apache loads libphp*.so from a modules directory that is two levels
1494
+ // below /usr, which the default one-level globs miss.
1495
+ [
1496
+ "**/usr/lib/apache2/modules/libphp*.so",
1497
+ "**/usr/lib/httpd/modules/libphp*.so",
1498
+ "**/usr/lib64/httpd/modules/libphp*.so",
1499
+ ],
1407
1500
  ),
1408
1501
  "shared_library",
1409
1502
  );
1503
+ // Go executables carry the toolchain they were built with in their
1504
+ // buildinfo header. The stdlib is statically linked, so that version
1505
+ // decides whether the binary applies Go's CVE-2026-111943 distrust list.
1506
+ const goBuildInventory = collectGoBuildInfo(executables, src);
1507
+ if (goBuildInventory.stdlibComponents.length) {
1508
+ pkgList.push(...goBuildInventory.stdlibComponents);
1509
+ }
1510
+ if (goBuildInventory.dependencies.length) {
1511
+ dependenciesList.push(...goBuildInventory.dependencies);
1512
+ }
1513
+ // Source-built PHP (the Docker Official image, FrankenPHP and custom
1514
+ // /usr/local builds) is unpackaged, so its version is only visible in the
1515
+ // binaries and the installed php_version.h.
1516
+ const phpBuildInventory = collectPhpBuildInfo(executables, sharedLibs, src);
1517
+ if (phpBuildInventory.runtimeComponents.length) {
1518
+ pkgList.push(...phpBuildInventory.runtimeComponents);
1519
+ }
1520
+ if (phpBuildInventory.dependencies.length) {
1521
+ dependenciesList.push(...phpBuildInventory.dependencies);
1522
+ }
1523
+ // Node.js from the Docker Official images, the release tarballs, nvm, fnm or
1524
+ // volta is unpackaged, so its version is only visible in the binary.
1525
+ const nodeBuildInventory = collectNodeBuildInfo(executables, sharedLibs, src);
1526
+ if (nodeBuildInventory.runtimeComponents.length) {
1527
+ pkgList.push(...nodeBuildInventory.runtimeComponents);
1528
+ }
1529
+ if (nodeBuildInventory.dependencies.length) {
1530
+ dependenciesList.push(...nodeBuildInventory.dependencies);
1531
+ }
1532
+ // Ruby from the Docker Official images, ruby-build, rbenv, rvm, asdf, mise
1533
+ // or chruby is unpackaged; its version is in rbconfig.rb and libruby.
1534
+ const rubyBuildInventory = collectRubyBuildInfo(executables, sharedLibs, src);
1535
+ if (rubyBuildInventory.runtimeComponents.length) {
1536
+ pkgList.push(...rubyBuildInventory.runtimeComponents);
1537
+ }
1538
+ if (rubyBuildInventory.dependencies.length) {
1539
+ dependenciesList.push(...rubyBuildInventory.dependencies);
1540
+ }
1541
+ // A Ruby an OS package installed is named by that package; it also gets
1542
+ // its upstream identity, without a CPE, so a BOM names the interpreter both
1543
+ // ways.
1544
+ pkgList.push(...deriveOsPackagedRubyRuntimes(pkgList));
1545
+ // The .NET images and dotnet-install.sh put the runtime under a dotnet root
1546
+ // without a package manager; its deps.json names the runtime pack.
1547
+ const dotnetRuntimes = collectDotnetRuntimes(src, ownedFilePaths);
1548
+ if (dotnetRuntimes.length) {
1549
+ pkgList.push(...dotnetRuntimes);
1550
+ for (const runtime of dotnetRuntimes) {
1551
+ dependenciesList.push({ ref: runtime["bom-ref"], dependsOn: [] });
1552
+ }
1553
+ }
1554
+ // A Brotli decoder compiled into a native file that no package manager owns
1555
+ // (Node.js, Python wheels, PHP and Ruby extensions, the .NET runtime, static
1556
+ // binaries) is only visible by reading the file.
1557
+ const brotliInventory = await collectBrotliDecoders(
1558
+ executables,
1559
+ sharedLibs,
1560
+ src,
1561
+ ownedFilePaths,
1562
+ );
1563
+
1564
+ if (brotliInventory.components.length) {
1565
+ pkgList.push(...brotliInventory.components);
1566
+ }
1567
+ if (brotliInventory.dependencies.length) {
1568
+ dependenciesList.push(...brotliInventory.dependencies);
1569
+ }
1410
1570
  return {
1411
1571
  osPackages: pkgList,
1412
1572
  osPackageFiles,
1573
+ // Extension modules that carry a Brotli decoder. They are reported apart
1574
+ // from sharedLibs so the unpackaged-library count still counts what the
1575
+ // loader's directories hold.
1576
+ nativeExtensionFiles: brotliInventory.extensionFiles,
1577
+ ownerByPath,
1413
1578
  dependenciesList,
1414
1579
  allTypes: Array.from(allTypes).sort(),
1415
1580
  bundledSdks: Array.from(bundledSdks).sort(),
@@ -1545,17 +1710,24 @@ function mergeOrganizationalEntityField(component, fieldName, entityName) {
1545
1710
  return { applied: false, represented: false };
1546
1711
  }
1547
1712
 
1548
- function parseOrganizationalContact(value) {
1713
+ /**
1714
+ * Split a `Name <address>` maintainer string into an `organizationalContact`.
1715
+ *
1716
+ * @param {string} value Maintainer or supplier string
1717
+ * @returns {{name: string, email?: string}|undefined} The contact, if any
1718
+ */
1719
+ export function parseOrganizationalContact(value) {
1549
1720
  const normalizedValue = `${value || ""}`.trim();
1550
1721
  if (!normalizedValue) {
1551
1722
  return undefined;
1552
1723
  }
1553
1724
  const match = normalizedValue.match(/^([^<>]+?)\s*<([^<>\s]+@[^<>\s]+)>$/);
1554
1725
  if (match) {
1555
- return {
1556
- name: match[1].trim(),
1557
- email: match[2].trim(),
1558
- };
1726
+ const name = match[1].trim();
1727
+ const email = match[2].trim();
1728
+ // `organizationalContact.email` is an `idn-email`; an address that is not
1729
+ // one (say `a@b@c`) would fail schema validation, so keep only the name.
1730
+ return isValidIdnEmail(email) ? { name, email } : { name };
1559
1731
  }
1560
1732
  return { name: normalizedValue };
1561
1733
  }
@@ -1644,13 +1816,34 @@ function promoteTrivyOsPackageIdentity(component, trivyMetadata) {
1644
1816
  async function collectRootfsRepositoryInventory(basePath, options = {}) {
1645
1817
  let trustedKeyComponents = [];
1646
1818
  let refsByPath = new Map();
1819
+ let trustMaterialDependencies = [];
1647
1820
  if (isCycloneDxComponentTypeEnabled("cryptographic-asset", options)) {
1648
1821
  ({ components: trustedKeyComponents, refsByPath } =
1649
1822
  await collectTrustedKeyComponents(basePath));
1650
- trustedKeyComponents = applyTrustMaterialEnhancements(
1823
+ ({
1824
+ components: trustedKeyComponents,
1825
+ dependencies: trustMaterialDependencies,
1826
+ } = applyTrustMaterialEnhancements(
1651
1827
  trustedKeyComponents,
1652
1828
  collectTrustInspectorRootfsInventory(basePath),
1653
- );
1829
+ basePath,
1830
+ ));
1831
+ if (!TRUSTINSPECTOR_BIN) {
1832
+ // No plugin: walk the same locations in JavaScript. With the plugin
1833
+ // present its rootfs report is the only producer, so nothing is
1834
+ // counted twice.
1835
+ const fallback = collectRootfsCertificates(basePath);
1836
+ const knownRefs = new Set(trustedKeyComponents.map((c) => c["bom-ref"]));
1837
+ for (const component of fallback.components) {
1838
+ if (!knownRefs.has(component["bom-ref"])) {
1839
+ knownRefs.add(component["bom-ref"]);
1840
+ trustedKeyComponents.push(component);
1841
+ }
1842
+ }
1843
+ trustMaterialDependencies = trustMaterialDependencies.concat(
1844
+ fallback.dependencies,
1845
+ );
1846
+ }
1654
1847
  }
1655
1848
  refsByPath = new Map(
1656
1849
  trustedKeyComponents
@@ -1692,6 +1885,7 @@ async function collectRootfsRepositoryInventory(basePath, options = {}) {
1692
1885
  });
1693
1886
  }
1694
1887
  }
1888
+ dependenciesList.push(...trustMaterialDependencies);
1695
1889
  return { components, dependenciesList };
1696
1890
  }
1697
1891
 
@@ -1877,7 +2071,9 @@ function executeTrustInspector(args, activity) {
1877
2071
  if (DEBUG_MODE) {
1878
2072
  console.log("Executing", TRUSTINSPECTOR_BIN, args.join(" "));
1879
2073
  }
1880
- const result = safeSpawnSync(TRUSTINSPECTOR_BIN, args);
2074
+ const result = safeSpawnSync(TRUSTINSPECTOR_BIN, args, {
2075
+ timeout: 10 * 60 * 1000,
2076
+ });
1881
2077
  if (result?.status !== 0 || result?.error) {
1882
2078
  if (DEBUG_MODE && (result?.stdout || result?.stderr)) {
1883
2079
  console.error(result.stdout, result.stderr);
@@ -1991,91 +2187,54 @@ function createTrustMaterialComponent(material) {
1991
2187
  ? [{ name: "cdx:crypto:expiresAt", value: material.expiresAt }]
1992
2188
  : []),
1993
2189
  ]);
1994
- let component;
1995
2190
  if (material.kind === "certificate") {
1996
- component = {
1997
- "bom-ref": `crypto/certificate/${encodeURIComponent(material.name || normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.serial || material.expiresAt || "unknown"}`,
1998
- name: material.name || basename(normalizedPath),
1999
- type: "cryptographic-asset",
2000
- version:
2001
- material.sha256 ||
2002
- material.serial ||
2003
- material.expiresAt ||
2004
- "configured",
2005
- hashes,
2006
- description: material.subject || normalizedPath,
2007
- cryptoProperties: {
2008
- assetType: "certificate",
2009
- algorithmProperties: {
2010
- executionEnvironment: "unknown",
2011
- implementationPlatform: "unknown",
2012
- },
2013
- certificateProperties: {
2014
- serialNumber: material.serial || undefined,
2015
- subjectName: material.subject || undefined,
2016
- issuerName: material.issuer || undefined,
2017
- notValidBefore: material.createdAt || undefined,
2018
- notValidAfter: material.expiresAt || undefined,
2019
- certificateFormat: material.format || "X.509",
2020
- certificateFileExtension: material.fileExtension || undefined,
2021
- fingerprint: material.fingerprint
2022
- ? { alg: "SHA-256", content: material.fingerprint }
2023
- : undefined,
2024
- },
2191
+ // Certificates are built by the shared collector in
2192
+ // applyTrustMaterialEnhancements; this legacy builder no longer produces
2193
+ // them, so a certificate material reaching here is skipped.
2194
+ return undefined;
2195
+ }
2196
+ const component = {
2197
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.keyId || "unknown"}`,
2198
+ name: material.name || basename(normalizedPath),
2199
+ type: "cryptographic-asset",
2200
+ version: material.sha256 || material.keyId || normalizedPath,
2201
+ hashes,
2202
+ cryptoProperties: {
2203
+ assetType: "related-crypto-material",
2204
+ relatedCryptoMaterialProperties: {
2205
+ type: "public-key",
2206
+ id:
2207
+ material.keyId ||
2208
+ material.fingerprint ||
2209
+ material.sha256 ||
2210
+ normalizedPath,
2211
+ state: trustMaterialState(material),
2025
2212
  },
2026
- properties: uniqueProperties(
2027
- sharedProperties.concat(
2028
- material.trustDomain
2029
- ? [{ name: "cdx:crypto:trustDomain", value: material.trustDomain }]
2030
- : [],
2031
- ),
2032
- ),
2033
- };
2034
- } else {
2035
- component = {
2036
- "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(normalizedPath)}@${material.sha256 ? `sha256:${material.sha256}` : material.keyId || "unknown"}`,
2037
- name: material.name || basename(normalizedPath),
2038
- type: "cryptographic-asset",
2039
- version: material.sha256 || material.keyId || normalizedPath,
2040
- hashes,
2041
- cryptoProperties: {
2042
- assetType: "related-crypto-material",
2043
- relatedCryptoMaterialProperties: {
2044
- type: "public-key",
2045
- id:
2046
- material.keyId ||
2047
- material.fingerprint ||
2048
- material.sha256 ||
2049
- normalizedPath,
2050
- state: trustMaterialState(material),
2213
+ },
2214
+ properties: uniqueProperties(
2215
+ sharedProperties.concat([
2216
+ {
2217
+ name: "cdx:crypto:trustDomain",
2218
+ value: material.trustDomain || deriveTrustedKeyDomain(normalizedPath),
2051
2219
  },
2052
- },
2053
- properties: uniqueProperties(
2054
- sharedProperties.concat([
2055
- {
2056
- name: "cdx:crypto:trustDomain",
2057
- value:
2058
- material.trustDomain || deriveTrustedKeyDomain(normalizedPath),
2059
- },
2060
- { name: "cdx:crypto:keyPath", value: normalizedPath },
2061
- {
2062
- name: "cdx:crypto:fileExtension",
2063
- value:
2064
- material.fileExtension ||
2065
- extname(normalizedPath).replace(/^\./, "") ||
2066
- "gpg",
2067
- },
2068
- ...(material?.keyId
2069
- ? [{ name: "cdx:crypto:keyId", value: material.keyId }]
2070
- : []),
2071
- ...(material?.userIds || []).map((value) => ({
2072
- name: "cdx:crypto:userId",
2073
- value,
2074
- })),
2075
- ]),
2076
- ),
2077
- };
2078
- }
2220
+ { name: "cdx:crypto:keyPath", value: normalizedPath },
2221
+ {
2222
+ name: "cdx:crypto:fileExtension",
2223
+ value:
2224
+ material.fileExtension ||
2225
+ extname(normalizedPath).replace(/^\./, "") ||
2226
+ "gpg",
2227
+ },
2228
+ ...(material?.keyId
2229
+ ? [{ name: "cdx:crypto:keyId", value: material.keyId }]
2230
+ : []),
2231
+ ...(material?.userIds || []).map((value) => ({
2232
+ name: "cdx:crypto:userId",
2233
+ value,
2234
+ })),
2235
+ ]),
2236
+ ),
2237
+ };
2079
2238
  attachIdentityTools(component, trustInspectorToolRefs());
2080
2239
  return component;
2081
2240
  }
@@ -2100,9 +2259,73 @@ function enhanceComponentFromTrustMaterial(component, material) {
2100
2259
  return component;
2101
2260
  }
2102
2261
 
2103
- function applyTrustMaterialEnhancements(components, materials) {
2262
+ function createKeystoreComponent(material) {
2263
+ const normalizedPath = normalizeContainerPath(material?.path);
2264
+ if (!normalizedPath) {
2265
+ return undefined;
2266
+ }
2267
+ const readable = (material?.properties || []).find(
2268
+ (property) => property.name === "cdx:crypto:keystoreReadable",
2269
+ )?.value;
2270
+ const component = {
2271
+ "bom-ref": `crypto/related-crypto-material/keystore/${encodeURIComponent(normalizedPath)}`,
2272
+ name: basename(normalizedPath),
2273
+ type: "cryptographic-asset",
2274
+ cryptoProperties: {
2275
+ assetType: "related-crypto-material",
2276
+ relatedCryptoMaterialProperties: {
2277
+ type: "other",
2278
+ ...(material?.format ? { format: material.format } : {}),
2279
+ },
2280
+ },
2281
+ evidence: { occurrences: [{ location: normalizedPath }] },
2282
+ properties: [
2283
+ { name: "internal:SrcFile", value: normalizedPath },
2284
+ ...(material?.sourceType
2285
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2286
+ : []),
2287
+ ...(readable
2288
+ ? [{ name: "cdx:crypto:keystoreReadable", value: readable }]
2289
+ : []),
2290
+ ],
2291
+ };
2292
+ attachIdentityTools(component, trustInspectorToolRefs());
2293
+ return component;
2294
+ }
2295
+
2296
+ // Private keys are classified from the PEM header line only and never
2297
+ // decoded (PLAN section 1.7): the format travels, no key material does.
2298
+ function createPrivateKeyComponent(material) {
2299
+ const normalizedPath = normalizeContainerPath(material?.path);
2300
+ if (!normalizedPath) {
2301
+ return undefined;
2302
+ }
2303
+ const component = {
2304
+ "bom-ref": `crypto/related-crypto-material/private-key/${encodeURIComponent(normalizedPath)}`,
2305
+ name: basename(normalizedPath),
2306
+ type: "cryptographic-asset",
2307
+ cryptoProperties: {
2308
+ assetType: "related-crypto-material",
2309
+ relatedCryptoMaterialProperties: {
2310
+ type: "private-key",
2311
+ ...(material?.format ? { format: material.format } : {}),
2312
+ },
2313
+ },
2314
+ evidence: { occurrences: [{ location: normalizedPath }] },
2315
+ properties: [
2316
+ { name: "internal:SrcFile", value: normalizedPath },
2317
+ ...(material?.sourceType
2318
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
2319
+ : []),
2320
+ ],
2321
+ };
2322
+ attachIdentityTools(component, trustInspectorToolRefs());
2323
+ return component;
2324
+ }
2325
+
2326
+ function applyTrustMaterialEnhancements(components, materials, basePath) {
2104
2327
  if (!materials?.length) {
2105
- return components || [];
2328
+ return { components: components || [], dependencies: [] };
2106
2329
  }
2107
2330
  const componentList = [...(components || [])];
2108
2331
  const bySrcFile = new Map();
@@ -2119,6 +2342,8 @@ function applyTrustMaterialEnhancements(components, materials) {
2119
2342
  }
2120
2343
  }
2121
2344
  for (const material of materials) {
2345
+ // GPG-style keyring materials (report-1 `kind: public-key`) enhance the
2346
+ // components the native readers already produced.
2122
2347
  const srcFile = normalizeContainerPath(material?.path);
2123
2348
  const existing = bySrcFile.get(srcFile);
2124
2349
  if (
@@ -2129,17 +2354,95 @@ function applyTrustMaterialEnhancements(components, materials) {
2129
2354
  enhanceComponentFromTrustMaterial(existing, material);
2130
2355
  continue;
2131
2356
  }
2132
- const component = createTrustMaterialComponent(material);
2357
+ if (material?.kind === "public-key") {
2358
+ const component = createTrustMaterialComponent(material);
2359
+ if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
2360
+ continue;
2361
+ }
2362
+ seenRefs.add(component["bom-ref"]);
2363
+ componentList.push(component);
2364
+ if (srcFile) {
2365
+ bySrcFile.set(srcFile, component);
2366
+ }
2367
+ }
2368
+ }
2369
+ // Certificates, their SPKI public keys, algorithm components and verified
2370
+ // issuer edges come from the shared collector; keystores and private keys
2371
+ // are their own components.
2372
+ const collector = new CertificateCollector();
2373
+ collector.addTrustInspectorMaterials(materials);
2374
+ const { components: cryptoComponents, dependencies } = collector.build();
2375
+ for (const material of materials) {
2376
+ if (material?.kind === "keystore") {
2377
+ cryptoComponents.push(createKeystoreComponent(material));
2378
+ } else if (material?.kind === "private-key") {
2379
+ cryptoComponents.push(createPrivateKeyComponent(material));
2380
+ }
2381
+ }
2382
+ // A keystore the plugin could read is reported as the certificates it
2383
+ // holds; give it the keystore component the unreadable ones get and link
2384
+ // it to those certificates.
2385
+ if (basePath) {
2386
+ const certMaterialByPath = new Map();
2387
+ for (const material of materials) {
2388
+ if (material?.kind === "certificate" && material.path) {
2389
+ certMaterialByPath.set(normalizeContainerPath(material.path), material);
2390
+ }
2391
+ }
2392
+ for (const [path, material] of certMaterialByPath) {
2393
+ let classification;
2394
+ try {
2395
+ const absolute = join(basePath, path.replace(/^\/+/, ""));
2396
+ // The path comes from the plugin report: it must stay inside the
2397
+ // scanned root, symlinks included.
2398
+ const realRoot = realpathSync(basePath);
2399
+ const realFile = realpathSync(absolute);
2400
+ const inside = relative(realRoot, realFile);
2401
+ if (inside.startsWith("..") || isAbsolute(inside)) {
2402
+ continue;
2403
+ }
2404
+ const stats = lstatSync(absolute);
2405
+ if (!stats.isFile() || stats.size > 16 * 1024 * 1024) {
2406
+ continue;
2407
+ }
2408
+ classification = classifyCryptoFile(readFileSync(absolute));
2409
+ } catch (_err) {
2410
+ continue;
2411
+ }
2412
+ if (classification.kind !== "keystore") {
2413
+ continue;
2414
+ }
2415
+ const keystore = createKeystoreComponent({
2416
+ path,
2417
+ format: classification.format,
2418
+ sourceType: material.sourceType,
2419
+ });
2420
+ cryptoComponents.push(keystore);
2421
+ const held = cryptoComponents
2422
+ .filter(
2423
+ (component) =>
2424
+ component.cryptoProperties?.assetType === "certificate" &&
2425
+ component.evidence?.occurrences?.some(
2426
+ (occurrence) =>
2427
+ normalizeContainerPath(occurrence.location) === path,
2428
+ ),
2429
+ )
2430
+ .map((component) => component["bom-ref"])
2431
+ .sort();
2432
+ if (held.length) {
2433
+ dependencies.push({ ref: keystore["bom-ref"], dependsOn: held });
2434
+ }
2435
+ }
2436
+ }
2437
+ for (const component of cryptoComponents) {
2133
2438
  if (!component?.["bom-ref"] || seenRefs.has(component["bom-ref"])) {
2134
2439
  continue;
2135
2440
  }
2136
2441
  seenRefs.add(component["bom-ref"]);
2442
+ attachIdentityTools(component, trustInspectorToolRefs());
2137
2443
  componentList.push(component);
2138
- if (srcFile) {
2139
- bySrcFile.set(srcFile, component);
2140
- }
2141
2444
  }
2142
- return componentList;
2445
+ return { components: componentList, dependencies };
2143
2446
  }
2144
2447
 
2145
2448
  function collectTrustInspectorRootfsInventory(basePath) {
@@ -2510,6 +2813,7 @@ async function createOSPackageFileComponents(basePath, osPackageEntries) {
2510
2813
  const components = [];
2511
2814
  const dependenciesList = [];
2512
2815
  const ownedFilePaths = new Set();
2816
+ const ownerByPath = new Map();
2513
2817
  const services = [];
2514
2818
  const componentByPath = new Map();
2515
2819
  for (const packageEntry of osPackageEntries) {
@@ -2520,6 +2824,10 @@ async function createOSPackageFileComponents(basePath, osPackageEntries) {
2520
2824
  continue;
2521
2825
  }
2522
2826
  ownedFilePaths.add(filePath);
2827
+ const ownerKey = filePath.replaceAll("\\", "/").replace(/^\/+/, "");
2828
+ if (!ownerByPath.has(ownerKey)) {
2829
+ ownerByPath.set(ownerKey, packageEntry.packageRef);
2830
+ }
2523
2831
  let fileComponent = componentByPath.get(filePath);
2524
2832
  if (!fileComponent) {
2525
2833
  fileComponent = await createOSPackageFileComponent(
@@ -2558,6 +2866,7 @@ async function createOSPackageFileComponents(basePath, osPackageEntries) {
2558
2866
  components,
2559
2867
  dependenciesList,
2560
2868
  ownedFilePaths: Array.from(ownedFilePaths).sort(),
2869
+ ownerByPath,
2561
2870
  services: dedupeServices(services),
2562
2871
  };
2563
2872
  }
@@ -3043,7 +3352,10 @@ function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
3043
3352
  bundledRuntimes.add(comp.name);
3044
3353
  }
3045
3354
  // TODO: Need to test this for a range of base images
3046
- if (COMMON_RUNTIMES.includes(comp.name)) {
3355
+ if (
3356
+ COMMON_RUNTIMES.includes(comp.name) ||
3357
+ VERSIONED_RUNTIME_PATTERNS.some((pattern) => pattern.test(comp.name))
3358
+ ) {
3047
3359
  bundledRuntimes.add(comp.name);
3048
3360
  }
3049
3361
  }
@@ -3299,10 +3611,37 @@ export function enrichOSComponentsWithTrustData(components = []) {
3299
3611
  if (hostComponents.length) {
3300
3612
  mergedComponents.push(...hostComponents);
3301
3613
  }
3614
+ // report-2 host mode enumerates the platform trust stores as materials;
3615
+ // they flow through the shared certificate model like any other producer.
3616
+ const hostCollector = new CertificateCollector();
3617
+ hostCollector.addTrustInspectorMaterials(hostData?.materials);
3618
+ const { components: hostMaterialComponents } = hostCollector.build();
3619
+ for (const material of hostData?.materials || []) {
3620
+ if (material?.kind === "keystore") {
3621
+ const component = createKeystoreComponent(material);
3622
+ if (component) {
3623
+ hostMaterialComponents.push(component);
3624
+ }
3625
+ } else if (material?.kind === "private-key") {
3626
+ const component = createPrivateKeyComponent(material);
3627
+ if (component) {
3628
+ hostMaterialComponents.push(component);
3629
+ }
3630
+ }
3631
+ }
3632
+ for (const component of hostMaterialComponents) {
3633
+ if (!component?.["bom-ref"]) {
3634
+ continue;
3635
+ }
3636
+ attachIdentityTools(component, trustInspectorToolRefs());
3637
+ mergedComponents.push(component);
3638
+ }
3302
3639
  return {
3303
3640
  components: mergedComponents,
3304
3641
  tools:
3305
- pathInspectionCandidates.length || hostComponents.length
3642
+ pathInspectionCandidates.length ||
3643
+ hostComponents.length ||
3644
+ hostMaterialComponents.length
3306
3645
  ? getPluginToolComponents(["trustinspector"])
3307
3646
  : [],
3308
3647
  };
@@ -3473,6 +3812,347 @@ export function getBinaryBom(src, binaryBomFile, deepMode) {
3473
3812
  return true;
3474
3813
  }
3475
3814
 
3815
+ // The Go standard library has no module path, so `pkg:golang/stdlib@v<x>` is
3816
+ // not a purl cdx-purl will build (the same reason trivy's stdlib purl is
3817
+ // dropped further up). The bom-ref below is the decoded form of that string,
3818
+ // exactly the ref trivy-produced stdlib components already carry, so both
3819
+ // paths land on one component. Trivy reports the stdlib version without the
3820
+ // `go` prefix (`v1.27.2`) while buildinfo keeps it (`go1.27.2`).
3821
+ function goStdlibRef(goVersion) {
3822
+ // "go1.26.10 X:boringcrypto" names the toolchain go1.26.10.
3823
+ return `pkg:golang/stdlib@v${goVersion.split(/\s/)[0].replace(/^go/, "")}`;
3824
+ }
3825
+
3826
+ /**
3827
+ * Read the Go buildinfo version of each executable file component and link it
3828
+ * to (or create) the matching `pkg:golang/stdlib` component. Mutates the
3829
+ * components in place by adding `cdx:go:buildVersion`.
3830
+ *
3831
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3832
+ * @param {string} basePath extracted rootfs the paths are relative to
3833
+ * @returns {{stdlibComponents: Object[], dependencies: Object[]}}
3834
+ */
3835
+ function collectGoBuildInfo(executables, basePath) {
3836
+ const stdlibComponents = [];
3837
+ const dependencies = [];
3838
+ if (!executables?.length) {
3839
+ return { stdlibComponents, dependencies };
3840
+ }
3841
+ const stdlibRefs = new Set();
3842
+ for (const component of executables) {
3843
+ if (!component?.properties) {
3844
+ continue;
3845
+ }
3846
+ const srcFile = component.properties.find(
3847
+ (property) => property.name === "internal:SrcFile",
3848
+ )?.value;
3849
+ if (!srcFile) {
3850
+ continue;
3851
+ }
3852
+ const goVersion = readGoBuildVersion(
3853
+ join(basePath, srcFile.replace(/^\/+/, "")),
3854
+ );
3855
+ if (!goVersion) {
3856
+ continue;
3857
+ }
3858
+ component.properties.push({
3859
+ name: "cdx:go:buildVersion",
3860
+ value: goVersion,
3861
+ });
3862
+ // A pre-1.18 toolchain has no version string to build a stdlib ref
3863
+ // from; the buildVersion property alone records the fact.
3864
+ if (goVersion === PRE_1_18_VERSION) {
3865
+ continue;
3866
+ }
3867
+ const stdlibRef = goStdlibRef(goVersion);
3868
+ if (!stdlibRefs.has(stdlibRef)) {
3869
+ stdlibRefs.add(stdlibRef);
3870
+ stdlibComponents.push({
3871
+ "bom-ref": stdlibRef,
3872
+ name: "stdlib",
3873
+ type: "library",
3874
+ version: stdlibRef.slice("pkg:golang/stdlib@".length),
3875
+ group: "",
3876
+ });
3877
+ dependencies.push({ ref: stdlibRef, dependsOn: [] });
3878
+ }
3879
+ if (component["bom-ref"]) {
3880
+ dependencies.push({
3881
+ ref: component["bom-ref"],
3882
+ dependsOn: [stdlibRef],
3883
+ });
3884
+ }
3885
+ }
3886
+ return { stdlibComponents, dependencies };
3887
+ }
3888
+
3889
+ /**
3890
+ * Read the PHP build version of each PHP-named executable and shared library,
3891
+ * add the cdx:php:* properties, and link the components to one platform
3892
+ * component per distinct version. Mutates the components in place, as the Go
3893
+ * collector does.
3894
+ *
3895
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3896
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
3897
+ * @param {string} basePath extracted rootfs the paths are relative to
3898
+ * @returns {{runtimeComponents: Object[], dependencies: Object[]}}
3899
+ */
3900
+ function collectPhpBuildInfo(executables, sharedLibs, basePath) {
3901
+ const inventory = createPhpRuntimeInventory();
3902
+ let probes = 0;
3903
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
3904
+ if (probes >= MAX_PHP_PROBES) {
3905
+ break;
3906
+ }
3907
+ if (!component?.properties) {
3908
+ continue;
3909
+ }
3910
+ const srcFile = component.properties.find(
3911
+ (property) => property.name === "internal:SrcFile",
3912
+ )?.value;
3913
+ if (!srcFile) {
3914
+ continue;
3915
+ }
3916
+ const sapi = classifyPhpSapi(srcFile);
3917
+ if (!sapi) {
3918
+ continue;
3919
+ }
3920
+ const absolutePath = join(basePath, srcFile.replace(/^\/+/, ""));
3921
+ let resolvedPath = srcFile;
3922
+ try {
3923
+ resolvedPath = relative(
3924
+ realpathSync(basePath),
3925
+ realpathSync(absolutePath),
3926
+ );
3927
+ } catch {
3928
+ // readPhpBuildVersion decides what an unresolvable path yields.
3929
+ }
3930
+ // Trivy does not list Nix, Guix or Homebrew packages, so a PHP in their
3931
+ // stores looks unpackaged here, but it is still theirs.
3932
+ if (packageStoreOf(srcFile) || packageStoreOf(resolvedPath)) {
3933
+ continue;
3934
+ }
3935
+ probes += 1;
3936
+ const build = readPhpBuildVersion(absolutePath, basePath);
3937
+ if (!build) {
3938
+ continue;
3939
+ }
3940
+ attachPhpBuild(inventory, component, build, sapi);
3941
+ }
3942
+ return {
3943
+ runtimeComponents: inventory.runtimeComponents,
3944
+ dependencies: inventory.dependencies,
3945
+ };
3946
+ }
3947
+
3948
+ /**
3949
+ * Read the Node.js build version of each node-named executable and libnode
3950
+ * shared library, add the cdx:node:* properties, and link the components to
3951
+ * one platform component per distinct version. Mutates the components in
3952
+ * place, as the PHP collector does.
3953
+ *
3954
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3955
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
3956
+ * @param {string} basePath extracted rootfs the paths are relative to
3957
+ * @returns {{runtimeComponents: Object[], dependencies: Object[]}}
3958
+ */
3959
+ function collectNodeBuildInfo(executables, sharedLibs, basePath) {
3960
+ const inventory = createNodeRuntimeInventory();
3961
+ let probes = 0;
3962
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
3963
+ if (probes >= MAX_NODE_PROBES) {
3964
+ break;
3965
+ }
3966
+ const srcFile = component?.properties?.find(
3967
+ (property) => property.name === "internal:SrcFile",
3968
+ )?.value;
3969
+ if (!srcFile || !(isNodeBinaryName(srcFile) || isLibnode(srcFile))) {
3970
+ continue;
3971
+ }
3972
+ if (packageStoreOf(srcFile)) {
3973
+ continue;
3974
+ }
3975
+ probes += 1;
3976
+ const build = readNodeBuildVersion(
3977
+ join(basePath, srcFile.replace(/^\/+/, "")),
3978
+ basePath,
3979
+ );
3980
+ if (build) {
3981
+ attachNodeBuild(inventory, component, build);
3982
+ }
3983
+ }
3984
+ return {
3985
+ runtimeComponents: inventory.runtimeComponents,
3986
+ dependencies: inventory.dependencies,
3987
+ };
3988
+ }
3989
+
3990
+ /**
3991
+ * Read the Ruby build version of each unpackaged ruby executable and libruby
3992
+ * shared library, add the cdx:ruby:* properties, and link the components to
3993
+ * one platform component per distinct version. Mutates the components in
3994
+ * place, as the PHP and Node.js collectors do.
3995
+ *
3996
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
3997
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
3998
+ * @param {string} basePath extracted rootfs the paths are relative to
3999
+ * @returns {{runtimeComponents: Object[], dependencies: Object[]}}
4000
+ */
4001
+ function collectRubyBuildInfo(executables, sharedLibs, basePath) {
4002
+ const inventory = createRubyRuntimeInventory();
4003
+ let probes = 0;
4004
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
4005
+ if (probes >= MAX_RUBY_PROBES) {
4006
+ break;
4007
+ }
4008
+ const srcFile = component?.properties?.find(
4009
+ (property) => property.name === "internal:SrcFile",
4010
+ )?.value;
4011
+ if (!srcFile || !(isRubyBinaryName(srcFile) || isLibrubyName(srcFile))) {
4012
+ continue;
4013
+ }
4014
+ if (packageStoreOf(srcFile)) {
4015
+ continue;
4016
+ }
4017
+ probes += 1;
4018
+ const build = readRubyBuildVersion(
4019
+ join(basePath, srcFile.replace(/^\/+/, "")),
4020
+ basePath,
4021
+ );
4022
+ if (build) {
4023
+ attachRubyBuild(inventory, component, build);
4024
+ }
4025
+ }
4026
+ return {
4027
+ runtimeComponents: inventory.runtimeComponents,
4028
+ dependencies: inventory.dependencies,
4029
+ };
4030
+ }
4031
+
4032
+ function isLibnode(srcFile) {
4033
+ const name = basename(srcFile.replaceAll("\\", "/"));
4034
+ return /^libnode\.so(\.\d+)*$/.test(name);
4035
+ }
4036
+
4037
+ /**
4038
+ * Probe unpackaged executables, shared libraries and native extension modules
4039
+ * for a compiled-in Brotli decoder. Executables and libraries already have file
4040
+ * components and are tagged in place; an extension module becomes a file
4041
+ * component only when it carries a decoder, so the BOM does not grow by every
4042
+ * extension in the image. Executables and libraries, and extension modules,
4043
+ * each get MAX_BROTLI_PROBES files, and the scan reads at most
4044
+ * MAX_BROTLI_PROBE_BYTES in all; what a cap leaves unread is logged and
4045
+ * recorded as the binary.brotli-probe.capped degradation.
4046
+ *
4047
+ * @param {Object[]} executables file components from `fileComponents(.., "executable")`
4048
+ * @param {Object[]} sharedLibs file components from `fileComponents(.., "shared_library")`
4049
+ * @param {string} basePath extracted rootfs the paths are relative to
4050
+ * @param {string[]} ownedFilePaths root-relative paths owned by OS packages
4051
+ * @returns {Promise<{components: Object[], dependencies: Object[], extensionFiles: Object[]}>}
4052
+ */
4053
+ async function collectBrotliDecoders(
4054
+ executables,
4055
+ sharedLibs,
4056
+ basePath,
4057
+ ownedFilePaths,
4058
+ ) {
4059
+ const inventory = createBrotliInventory();
4060
+ let bytesRead = 0;
4061
+ const unread = { files: 0, extensions: 0 };
4062
+ const probe = (relativePath) => {
4063
+ if (bytesRead >= MAX_BROTLI_PROBE_BYTES) {
4064
+ return undefined;
4065
+ }
4066
+ const decoder = readBrotliDecoder(join(basePath, relativePath), basePath);
4067
+ if (decoder && !decoder.skipped) {
4068
+ bytesRead += decoder.size || 0;
4069
+ }
4070
+ return decoder;
4071
+ };
4072
+ let probes = 0;
4073
+ const seenPaths = new Set();
4074
+ for (const component of [...(executables || []), ...(sharedLibs || [])]) {
4075
+ const srcFile = component?.properties?.find(
4076
+ (property) => property.name === "internal:SrcFile",
4077
+ )?.value;
4078
+ if (!srcFile) {
4079
+ continue;
4080
+ }
4081
+ const relativePath = srcFile.replaceAll("\\", "/").replace(/^\/+/, "");
4082
+ seenPaths.add(relativePath);
4083
+ if (packageStoreOf(relativePath)) {
4084
+ continue;
4085
+ }
4086
+ if (probes >= MAX_BROTLI_PROBES || bytesRead >= MAX_BROTLI_PROBE_BYTES) {
4087
+ unread.files += 1;
4088
+ continue;
4089
+ }
4090
+ probes += 1;
4091
+ const decoder = probe(relativePath);
4092
+ if (decoder) {
4093
+ attachBrotliDecoder(inventory, component, decoder);
4094
+ }
4095
+ }
4096
+ const extensionHits = [];
4097
+ let extensionProbes = 0;
4098
+ for (const relativePath of collectNativeExtensions(basePath, [
4099
+ ...(ownedFilePaths || []),
4100
+ ...seenPaths,
4101
+ ])) {
4102
+ if (packageStoreOf(relativePath)) {
4103
+ continue;
4104
+ }
4105
+ if (
4106
+ extensionProbes >= MAX_BROTLI_PROBES ||
4107
+ bytesRead >= MAX_BROTLI_PROBE_BYTES
4108
+ ) {
4109
+ unread.extensions += 1;
4110
+ continue;
4111
+ }
4112
+ extensionProbes += 1;
4113
+ const decoder = probe(relativePath);
4114
+ if (decoder?.c || decoder?.go || decoder?.skipped) {
4115
+ extensionHits.push({ relativePath, decoder });
4116
+ }
4117
+ }
4118
+ if (unread.files || unread.extensions) {
4119
+ const detail = `The Brotli decoder probe left ${unread.files} executable(s) or librar(ies) and ${unread.extensions} extension module(s) unread after reaching its file or byte cap, so an embedded decoder in them is not reported.`;
4120
+ thoughtLog(detail);
4121
+ recordDegradation("binary.brotli-probe.capped", {
4122
+ ecosystem: "generic",
4123
+ tool: "brotli-probe",
4124
+ impact: "components",
4125
+ path: basePath,
4126
+ detail,
4127
+ });
4128
+ }
4129
+ const extensionFiles = [];
4130
+ if (extensionHits.length) {
4131
+ const components = await fileComponents(
4132
+ basePath,
4133
+ extensionHits.map((hit) => hit.relativePath),
4134
+ "shared_library",
4135
+ );
4136
+ for (let index = 0; index < components.length; index++) {
4137
+ const component = components[index];
4138
+ // Extension modules often share a basename (brotli.so is both a PHP and
4139
+ // a Ruby extension), so the path keeps each one a component of its own.
4140
+ const purl = tryBuildPurl({
4141
+ type: "generic",
4142
+ name: component.name,
4143
+ subpath: extensionHits[index].relativePath,
4144
+ });
4145
+ if (purl) {
4146
+ component.purl = purl;
4147
+ component["bom-ref"] = purl;
4148
+ }
4149
+ attachBrotliDecoder(inventory, component, extensionHits[index].decoder);
4150
+ extensionFiles.push(component);
4151
+ }
4152
+ }
4153
+ return { ...finishBrotliInventory(inventory), extensionFiles };
4154
+ }
4155
+
3476
4156
  async function fileComponents(basePath, fileList, fileType) {
3477
4157
  const hashResults = await mapWithConcurrency(fileList, (f) =>
3478
4158
  multiChecksumFile(["md5", "sha1"], join(basePath, f))