@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,11 +1,25 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, statSync } from "node:fs";
2
2
  import { platform as _platform } from "node:os";
3
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
+ import {
4
+ basename,
5
+ dirname,
6
+ extname,
7
+ isAbsolute,
8
+ join,
9
+ relative,
10
+ resolve,
11
+ } from "node:path";
4
12
  import process from "node:process";
5
13
 
6
14
  import { build } from "@cdxgen/cdx-purl";
7
15
 
8
- import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
16
+ import {
17
+ DEBUG_MODE,
18
+ hasDangerousUnicode,
19
+ isDryRun,
20
+ readEnvironmentVariable,
21
+ recordActivity,
22
+ } from "../core/activity.js";
9
23
  import { recordDegradation } from "../core/buildLedger.js";
10
24
  import { deferFailOnError } from "../core/deferredExit.js";
11
25
  import {
@@ -26,7 +40,10 @@ import {
26
40
  safeWriteSync,
27
41
  } from "../core/fs.js";
28
42
  import { thoughtLog } from "../core/logger.js";
29
- import { mapWithConcurrency } from "../core/parallel.js";
43
+ import {
44
+ isProjectToolingAllowed,
45
+ reportProjectToolingSkipped,
46
+ } from "../core/projectTooling.js";
30
47
  import {
31
48
  extractJarArchive,
32
49
  getNugetMetadata,
@@ -35,7 +52,9 @@ import {
35
52
  } from "../ecosystems/ecosystems.js";
36
53
  import {
37
54
  agreedNugetVersions,
55
+ applyNugetVersion,
38
56
  dotnetManifestProjectDir,
57
+ dotnetRestoredProjectFile,
39
58
  getCentralPackageVersions,
40
59
  indexPropsProperties,
41
60
  parseCsPkgData,
@@ -83,11 +102,14 @@ import {
83
102
  addRubyGemsDistributionUrls,
84
103
  addRubyRuntimeComponent,
85
104
  collectGemModuleNames,
105
+ collectProjectRubyDeclarations,
86
106
  describeProjectGem,
87
107
  enrichGemsFromLocalCache,
88
108
  parseBundleConfig,
89
109
  parseGemfileLockData,
90
110
  parseGemspecData,
111
+ resolveProjectRuby,
112
+ toGemPurl,
91
113
  } from "../ecosystems/rubyutils.js";
92
114
  import { getTerraformRegistryMetadata } from "../ecosystems/terraformRegistry.js";
93
115
  import {
@@ -100,6 +122,15 @@ import {
100
122
  collectDosaiCryptoComponents,
101
123
  collectSourceCryptoComponents,
102
124
  } from "../inventory/cbomutils.js";
125
+ import {
126
+ androidNetworkSecurityConfigPins,
127
+ CertificateCollector,
128
+ certRecordFromDer,
129
+ classifyCryptoFile,
130
+ containerRelativePath,
131
+ kubernetesTlsSecretRecords,
132
+ linkFallbackIssuers,
133
+ } from "../inventory/certificates.js";
103
134
  import { readZipEntry } from "../inventory/deps.js";
104
135
  import {
105
136
  filterInvalidCryptoComponents,
@@ -113,14 +144,23 @@ import {
113
144
  collectDosaiServiceComponents,
114
145
  collectDosaiServicesFromMethods,
115
146
  createDosaiMethodsSlice,
116
- dosaiRunWasStopped,
147
+ dosaiFailureReported,
148
+ dosaiMethodsSectionsArgument,
149
+ dosaiReportFileOmittedSections,
150
+ dosaiToolComponents,
117
151
  isDosaiDotnetLanguage,
118
152
  normalizeDosaiServiceMap,
119
153
  readDosaiMethodsReport,
154
+ runsEvidenceAnalysis,
120
155
  } from "../inventory/dosai.js";
121
156
  import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
157
+ import { resolvePluginBinary } from "../inventory/plugins.js";
122
158
  import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
123
- import { classifyProbeResult } from "../inventory/toolRequirements.js";
159
+ import { isInsideInstalledGem } from "../inventory/rubyBuildInfo.js";
160
+ import {
161
+ classifyProbeResult,
162
+ parseGemfileLockBundlerVersion,
163
+ } from "../inventory/toolRequirements.js";
124
164
  import { getPluginToolComponents } from "../managers/binary.js";
125
165
  import { getTreeWithPlugin } from "../managers/piptree.js";
126
166
  import {
@@ -225,11 +265,14 @@ export function createPixiBom(path, options) {
225
265
  generatePixiLockFile(path);
226
266
 
227
267
  const pixiLockFile = join(path, "pixi.lock");
228
- if (!safeExistsSync(pixiLockFile) && DEBUG_MODE) {
229
- console.log(
230
- "Unexpected Error tried to generate pixi.lock file but failed.",
231
- );
232
- console.log("This will result in creations of empty BOM.");
268
+ if (!safeExistsSync(pixiLockFile)) {
269
+ if (DEBUG_MODE) {
270
+ console.log(
271
+ "Unexpected Error tried to generate pixi.lock file but failed.",
272
+ );
273
+ }
274
+ // Let createPythonBOM() generate the BOM from the other manifests.
275
+ return null;
233
276
  }
234
277
  PixiLockData = parsePixiLockFile(pixiLockFile);
235
278
  metadataFilename = "pixi.lock";
@@ -494,11 +537,14 @@ export async function createPythonBom(path, options) {
494
537
  retMap.dependenciesList,
495
538
  parentComponent,
496
539
  );
497
- } else {
540
+ } else if (isProjectToolingAllowed(options)) {
541
+ // poetry export and pdm export load plugins from the project tree
542
+ // (.poetry/plugins, .pdm-plugins), so they run only when the
543
+ // project's own tooling may.
498
544
  let exportedReqs = "";
499
545
  if (f.endsWith("poetry.lock")) {
500
546
  thoughtLog(
501
- "Using poetry export as a safe, static alternative to pip install.",
547
+ "Using poetry export to collect the resolved closure without installing anything.",
502
548
  );
503
549
  const expCmd = safeSpawnSync(
504
550
  "poetry",
@@ -509,7 +555,7 @@ export async function createPythonBom(path, options) {
509
555
  exportedReqs = expCmd.stdout.toString();
510
556
  } else if (f.endsWith("pdm.lock")) {
511
557
  thoughtLog(
512
- "Using pdm export as a safe, static alternative to pip install.",
558
+ "Using pdm export to collect the resolved closure without installing anything.",
513
559
  );
514
560
  const expCmd = safeSpawnSync(
515
561
  "pdm",
@@ -539,6 +585,19 @@ export async function createPythonBom(path, options) {
539
585
  pkgList = pkgList.concat(dlist);
540
586
  }
541
587
  }
588
+ } else {
589
+ reportProjectToolingSkipped(
590
+ options,
591
+ "python.poetry-export.tooling-disabled",
592
+ "Skipping 'poetry export' and 'pdm export' since dependency installation is disabled. Only the packages parsed from the lock file will be used.",
593
+ {
594
+ ecosystem: "python",
595
+ tool: f.endsWith("pdm.lock") ? "pdm" : "poetry",
596
+ impact: "transitive-deps",
597
+ detail:
598
+ "Dependency installation is disabled, so poetry or pdm export was not run and only the packages parsed from the lock file are available.",
599
+ },
600
+ );
542
601
  }
543
602
  }
544
603
  }
@@ -879,9 +938,11 @@ export async function createPythonBom(path, options) {
879
938
  );
880
939
  }
881
940
  emitAiInventorySummary(aiInventory, path);
882
- // Check and complete the dependency tree
941
+ // Check and complete the dependency tree. This installs every package into a
942
+ // virtual environment, so --no-install-deps turns it off too.
883
943
  if (
884
944
  isFeatureEnabled(options, "safe-pip-install") &&
945
+ options.installDeps !== false &&
885
946
  pkgList.length &&
886
947
  isPartialTree(dependencies, pkgList.length)
887
948
  ) {
@@ -1108,6 +1169,28 @@ export function createHelmBom(path, options) {
1108
1169
  return {};
1109
1170
  }
1110
1171
 
1172
+ /**
1173
+ * The properties of a list followed by those of another it does not hold yet.
1174
+ * It returns a new array, so a shallow copy of a component keeps its own
1175
+ * properties.
1176
+ *
1177
+ * @param {{name: string, value: string}[]|undefined} existing Properties held so far
1178
+ * @param {{name: string, value: string}[]} additions Properties to add
1179
+ * @returns {{name: string, value: string}[]} The merged properties
1180
+ */
1181
+ function withMissingProperties(existing, additions) {
1182
+ const held = existing || [];
1183
+ return [
1184
+ ...held,
1185
+ ...additions.filter(
1186
+ (added) =>
1187
+ !held.some(
1188
+ (known) => known.name === added.name && known.value === added.value,
1189
+ ),
1190
+ ),
1191
+ ];
1192
+ }
1193
+
1111
1194
  /**
1112
1195
  * Function to create bom string for php projects
1113
1196
  *
@@ -1140,6 +1223,10 @@ export function createPHPBom(path, options) {
1140
1223
  composerLockSearchOptions,
1141
1224
  );
1142
1225
  let pkgList = [];
1226
+ // Platform facts of the scanned directory's lock when its composer.json names
1227
+ // no project: there is no module component to carry them, so they go on the
1228
+ // document. A nested lock like that has nowhere to put them.
1229
+ let metadataProperties = [];
1143
1230
  const composerJsonMode = composerJsonFiles.length;
1144
1231
  const composerLockMode = composerLockFiles.length;
1145
1232
  // Create a composer.lock file for each composer.json file if needed.
@@ -1253,6 +1340,37 @@ export function createPHPBom(path, options) {
1253
1340
  }
1254
1341
  }
1255
1342
  const retMap = parseComposerLock(f, rootRequires);
1343
+ if (retMap.platformProperties?.length) {
1344
+ if (moduleParent?.["bom-ref"]) {
1345
+ // The root composer.json is parsed twice, once for the parent
1346
+ // component and once per lock, so the object that ends up as
1347
+ // metadata.component is not always the module parsed here.
1348
+ for (const module of [moduleParent, parentComponent]) {
1349
+ if (module?.["bom-ref"] === moduleParent["bom-ref"]) {
1350
+ module.properties = withMissingProperties(
1351
+ module.properties,
1352
+ retMap.platformProperties,
1353
+ );
1354
+ }
1355
+ }
1356
+ } else if (resolve(basePath) === resolve(path)) {
1357
+ // The document describes the scanned directory, so only the lock
1358
+ // in it may put its facts on the document.
1359
+ metadataProperties = withMissingProperties(
1360
+ metadataProperties,
1361
+ retMap.platformProperties,
1362
+ );
1363
+ } else {
1364
+ recordDegradation("php.composer.platform-unattributed", {
1365
+ ecosystem: "php",
1366
+ tool: "composer",
1367
+ impact: "versions",
1368
+ path: f,
1369
+ detail:
1370
+ "The composer.json beside this lock names no project, so no component can carry the PHP and extension requirements of the lock.",
1371
+ });
1372
+ }
1373
+ }
1256
1374
  if (retMap.pkgList?.length) {
1257
1375
  pkgList = pkgList.concat(retMap.pkgList);
1258
1376
  pkgList = trimComponents(pkgList);
@@ -1292,6 +1410,7 @@ export function createPHPBom(path, options) {
1292
1410
  filename: composerLockFiles.join(", "),
1293
1411
  dependencies,
1294
1412
  parentComponent,
1413
+ metadataProperties,
1295
1414
  });
1296
1415
  }
1297
1416
  if (composerJsonMode && !composerLockMode) {
@@ -1318,7 +1437,14 @@ export async function createRubyBom(path, options) {
1318
1437
  if (typeof options.includeNodeModulesDir === "undefined" || options.deep) {
1319
1438
  options.includeNodeModulesDir = true;
1320
1439
  }
1321
- const excludeList = (options.exclude || []).concat(["**/vendor/cache/**"]);
1440
+ // RubyGems caches the specs it fetched from a source under ~/.cache/gem/specs
1441
+ // and ~/.gem/specs, Marshal-dumped into files named <gem>-<version>.gemspec.
1442
+ // Those describe what a source offered, not what is installed.
1443
+ const excludeList = (options.exclude || []).concat([
1444
+ "**/vendor/cache/**",
1445
+ "**/.cache/gem/specs/**",
1446
+ "**/.gem/specs/**",
1447
+ ]);
1322
1448
  const gemLockExcludeList = (options.exclude || []).concat([
1323
1449
  "**/vendor/bundle/ruby/**/Gemfile.lock",
1324
1450
  "**/test/data/**/Gemfile*.lock",
@@ -1344,6 +1470,12 @@ export async function createRubyBom(path, options) {
1344
1470
  exclude: gemLockExcludeList,
1345
1471
  },
1346
1472
  );
1473
+ // An installed gem's own directory can hold its development Gemfile.lock,
1474
+ // nested gemspecs and manifests (rbs ships steep/Gemfile.lock, typeprof a
1475
+ // VS Code extension). They describe how the gem is developed, not what is
1476
+ // installed, and would add gems that are not there.
1477
+ const isGemInternalFile = (f) =>
1478
+ isInsideInstalledGem(f, safeExistsSync, path);
1347
1479
  let gemspecFiles = getAllFiles(
1348
1480
  path,
1349
1481
  `${options.multiProject ? "**/" : ""}*.gemspec`,
@@ -1351,7 +1483,7 @@ export async function createRubyBom(path, options) {
1351
1483
  ...options,
1352
1484
  exclude: excludeList,
1353
1485
  },
1354
- );
1486
+ ).filter((f) => !isGemInternalFile(f));
1355
1487
  let gemHome =
1356
1488
  readEnvironmentVariable("CDXGEN_GEM_HOME") ||
1357
1489
  readEnvironmentVariable("GEM_HOME");
@@ -1432,7 +1564,9 @@ export async function createRubyBom(path, options) {
1432
1564
  ...options,
1433
1565
  exclude: gemLockExcludeList,
1434
1566
  },
1435
- );
1567
+ ).filter((f) => !isGemInternalFile(f));
1568
+ // The lockfiles each Bundler release resolved
1569
+ const bundlerVersions = new Map();
1436
1570
  if (gemLockFiles.length) {
1437
1571
  for (const f of gemLockFiles) {
1438
1572
  if (DEBUG_MODE) {
@@ -1440,6 +1574,15 @@ export async function createRubyBom(path, options) {
1440
1574
  }
1441
1575
  const gemLockData = readFileSync(f, { encoding: "utf-8" });
1442
1576
  const retMap = await parseGemfileLockData(gemLockData, f, { gemHome });
1577
+ // Bundler is not listed among the specs, but runs every bundle: the
1578
+ // lockfile names the release that resolved it under BUNDLED WITH.
1579
+ const bundlerVersion = parseGemfileLockBundlerVersion(gemLockData);
1580
+ if (bundlerVersion) {
1581
+ bundlerVersions.set(bundlerVersion, [
1582
+ ...(bundlerVersions.get(bundlerVersion) || []),
1583
+ f,
1584
+ ]);
1585
+ }
1443
1586
  if (retMap.pkgList?.length) {
1444
1587
  pkgList = pkgList.concat(retMap.pkgList);
1445
1588
  pkgList = trimComponents(pkgList);
@@ -1522,6 +1665,42 @@ export async function createRubyBom(path, options) {
1522
1665
  options,
1523
1666
  ));
1524
1667
  }
1668
+ for (const [bundlerVersion, lockFiles] of [...bundlerVersions.entries()].sort(
1669
+ (a, b) => a[0].localeCompare(b[0]),
1670
+ )) {
1671
+ const purl = toGemPurl("bundler", bundlerVersion);
1672
+ const ref = decodeURIComponent(purl);
1673
+ if (pkgList.some((p) => p["bom-ref"] === ref)) {
1674
+ continue;
1675
+ }
1676
+ pkgList.push({
1677
+ name: "bundler",
1678
+ version: bundlerVersion,
1679
+ purl,
1680
+ "bom-ref": ref,
1681
+ scope: "required",
1682
+ properties: [
1683
+ { name: "cdx:gem:bundledWith", value: "true" },
1684
+ { name: "internal:SrcFile", value: lockFiles.join(", ") },
1685
+ ],
1686
+ evidence: {
1687
+ identity: [
1688
+ {
1689
+ field: "purl",
1690
+ confidence: 0.8,
1691
+ methods: [
1692
+ {
1693
+ technique: "manifest-analysis",
1694
+ confidence: 0.8,
1695
+ value: lockFiles.join(", "),
1696
+ },
1697
+ ],
1698
+ },
1699
+ ],
1700
+ },
1701
+ });
1702
+ rootList.push(ref);
1703
+ }
1525
1704
  if (rootList.length) {
1526
1705
  dependencies = mergeDependencies(
1527
1706
  dependencies,
@@ -1539,7 +1718,14 @@ export async function createRubyBom(path, options) {
1539
1718
  options.resolveClass ||
1540
1719
  (options.deep && !hasAnyProjectType(["oci"], options, false))
1541
1720
  ) {
1542
- if (gemHome && !isGemHomeEmpty) {
1721
+ // `bundle exec` evaluates the project's Gemfile, a Ruby program
1722
+ if (gemHome && !isGemHomeEmpty && !isProjectToolingAllowed(options)) {
1723
+ reportProjectToolingSkipped(
1724
+ options,
1725
+ null,
1726
+ "Skipping the module names of gems since dependency installation is disabled. Collecting them runs 'bundle exec', which evaluates the Gemfile.",
1727
+ );
1728
+ } else if (gemHome && !isGemHomeEmpty) {
1543
1729
  const rubyCommand =
1544
1730
  readEnvironmentVariable("CDXGEN_RUBY_CMD") ||
1545
1731
  readEnvironmentVariable("RUBY_CMD") ||
@@ -1619,18 +1805,76 @@ export async function createRubyBom(path, options) {
1619
1805
  pkgList = await enrichGemsFromLocalCache(pkgList, { gemHome });
1620
1806
  // The download URL of a gem from the public registry needs no network call
1621
1807
  pkgList = addRubyGemsDistributionUrls(pkgList);
1622
- // Gems declare the Ruby they run on; surface it as the runtime they depend
1623
- // on. The component says the runtime is external, which only CycloneDX 1.7
1624
- // can express, and a container image ships its own Ruby.
1625
- if (
1626
- options.specVersion >= 1.7 &&
1627
- !hasAnyProjectType(["oci"], options, false)
1628
- ) {
1629
- ({ pkgList, dependencies } = addRubyRuntimeComponent(
1630
- pkgList,
1631
- parentComponent,
1632
- dependencies,
1633
- ));
1808
+ // The project and its gems declare the Ruby they run on; surface it as the
1809
+ // runtime they depend on. A pinned Ruby (Gemfile.lock, .ruby-version,
1810
+ // .tool-versions, mise, the Gemfile) is a version with a CPE, which every
1811
+ // spec version can carry. A bare range needs CycloneDX 1.7's versionRange
1812
+ // and isExternal. A container image ships its own Ruby, which the image
1813
+ // scan reads from the interpreter's files.
1814
+ if (!hasAnyProjectType(["oci"], options, false)) {
1815
+ // Each project of a multi-project scan pins its own Ruby
1816
+ const rootDir = resolve(path);
1817
+ const projectDirs = [rootDir];
1818
+ if (options.multiProject) {
1819
+ for (const f of [...gemLockFiles, ...gemFiles]) {
1820
+ const dir = dirname(resolve(f));
1821
+ if (!projectDirs.includes(dir)) {
1822
+ projectDirs.push(dir);
1823
+ }
1824
+ }
1825
+ }
1826
+ const rubies = [];
1827
+ for (const dir of projectDirs) {
1828
+ const ruby = resolveProjectRuby(collectProjectRubyDeclarations(dir));
1829
+ if (!ruby) {
1830
+ continue;
1831
+ }
1832
+ if (dir !== rootDir && ruby.source) {
1833
+ ruby.source = relative(rootDir, join(dir, ruby.source)).replaceAll(
1834
+ "\\",
1835
+ "/",
1836
+ );
1837
+ }
1838
+ if (ruby.conflicts?.length) {
1839
+ console.log(
1840
+ `NOTE: The project in ${dir} declares more than one Ruby. The BOM records ${ruby.engine} ${ruby.version} from ${ruby.source}; also declared: ${ruby.conflicts.join(", ")}.`,
1841
+ );
1842
+ }
1843
+ rubies.push(ruby);
1844
+ }
1845
+ // One runtime per distinct pinned Ruby; the scanned directory's first
1846
+ const pinned = [];
1847
+ for (const ruby of rubies) {
1848
+ if (
1849
+ ruby.version &&
1850
+ !pinned.some(
1851
+ (other) =>
1852
+ other.engine === ruby.engine && other.version === ruby.version,
1853
+ )
1854
+ ) {
1855
+ pinned.push(ruby);
1856
+ }
1857
+ }
1858
+ const [primary, ...others] = pinned.length ? pinned : rubies.slice(0, 1);
1859
+ if (options.specVersion >= 1.7 || primary?.version) {
1860
+ ({ pkgList, dependencies } = addRubyRuntimeComponent(
1861
+ pkgList,
1862
+ parentComponent,
1863
+ dependencies,
1864
+ primary,
1865
+ ));
1866
+ }
1867
+ // The gems were linked to the first; the others hang off the project
1868
+ for (const ruby of others) {
1869
+ let extra;
1870
+ ({ pkgList: extra, dependencies } = addRubyRuntimeComponent(
1871
+ [],
1872
+ parentComponent,
1873
+ dependencies,
1874
+ ruby,
1875
+ ));
1876
+ pkgList = pkgList.concat(extra);
1877
+ }
1634
1878
  }
1635
1879
  return buildBomNSData(options, pkgList, "gem", {
1636
1880
  src: path,
@@ -1655,6 +1899,12 @@ export async function createCsharpBom(path, options) {
1655
1899
  return createBinaryBom(path, options);
1656
1900
  }
1657
1901
  let parentComponent = createDefaultParentComponent(path, "nuget", options);
1902
+ // The directory a scan starts from is not a package, so it has a version
1903
+ // only when --project-version gives it one; "latest" is not a version.
1904
+ applyNugetVersion(
1905
+ parentComponent,
1906
+ options.projectVersion ? `${options.projectVersion}` : undefined,
1907
+ );
1658
1908
  const slnFiles = getAllFiles(
1659
1909
  path,
1660
1910
  `${options.multiProject ? "**/" : ""}*.sln*`,
@@ -1939,6 +2189,47 @@ export async function createCsharpBom(path, options) {
1939
2189
  // one project's manifest are not dropped just because another format exists
1940
2190
  // elsewhere in the scan.
1941
2191
  const assetsProjectDirs = new Set();
2192
+ // Restore records a project file the way the build was pointed at it, which
2193
+ // on a case-insensitive file system can differ in case from the file found
2194
+ // here. Such a path takes the found file's spelling, unless that is ambiguous.
2195
+ const foundProjectFiles = new Set();
2196
+ const foundProjectFilesByKey = new Map();
2197
+ for (const f of csProjFiles) {
2198
+ const file = resolve(f);
2199
+ const key = file.toLowerCase();
2200
+ foundProjectFiles.add(file);
2201
+ foundProjectFilesByKey.set(
2202
+ key,
2203
+ foundProjectFilesByKey.has(key) ? null : file,
2204
+ );
2205
+ }
2206
+ const foundProjectFile = (file) =>
2207
+ !file || foundProjectFiles.has(file)
2208
+ ? file
2209
+ : foundProjectFilesByKey.get(file.toLowerCase()) || file;
2210
+ // The package id and version each project's restore output records, by
2211
+ // project file. A project whose restore outputs disagree, such as a stale
2212
+ // obj/ beside an artifacts directory, takes neither.
2213
+ const restoredIdentities = new Map();
2214
+ const recordRestoredIdentity = (projectFile, identity) => {
2215
+ if (!projectFile || !identity) {
2216
+ return;
2217
+ }
2218
+ const known = restoredIdentities.get(projectFile);
2219
+ if (known === undefined) {
2220
+ restoredIdentities.set(projectFile, identity);
2221
+ } else if (
2222
+ known &&
2223
+ (known.name !== identity.name || known.version !== identity.version)
2224
+ ) {
2225
+ if (DEBUG_MODE) {
2226
+ console.log(
2227
+ `Restore outputs disagree on the package id or version of ${projectFile}, so the identity its project file states is used.`,
2228
+ );
2229
+ }
2230
+ restoredIdentities.set(projectFile, null);
2231
+ }
2232
+ };
1942
2233
  // project.assets.json parsing
1943
2234
  if (projAssetsFiles.length) {
1944
2235
  manifestFiles = manifestFiles.concat(projAssetsFiles);
@@ -1952,9 +2243,13 @@ export async function createCsharpBom(path, options) {
1952
2243
  const dlist = results["pkgList"];
1953
2244
  // Restore output names its project; an artifacts layout keeps it outside
1954
2245
  // the project directory.
1955
- const projectDir = dotnetManifestProjectDir(af, results.projectPath);
2246
+ const projectFile = foundProjectFile(
2247
+ dotnetRestoredProjectFile(af, results.projectPath, results.outputPath),
2248
+ );
2249
+ const projectDir = dotnetManifestProjectDir(af, projectFile);
1956
2250
  assetsProjectDirs.add(projectDir);
1957
2251
  recordProjectVersions(projectDir, dlist);
2252
+ recordRestoredIdentity(projectFile, results.projectIdentity);
1958
2253
  if (dlist?.length) {
1959
2254
  pkgList = pkgList.concat(dlist);
1960
2255
  }
@@ -2065,6 +2360,7 @@ export async function createCsharpBom(path, options) {
2065
2360
  }
2066
2361
  const projectRefByDir = {};
2067
2362
  let services = [];
2363
+ let dosaiTools = [];
2068
2364
  if (csProjFiles.length) {
2069
2365
  manifestFiles = manifestFiles.concat(csProjFiles);
2070
2366
  // Parsing csproj is quite error-prone. Some project files may not have
@@ -2073,11 +2369,18 @@ export async function createCsharpBom(path, options) {
2073
2369
  // whole scan agrees on.
2074
2370
  const agreedVersions = agreedNugetVersions(pkgList);
2075
2371
  let msbuildInstalled = false;
2076
- const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
2077
- shell: isWin,
2078
- });
2079
- if (result.status === 0 && !result.error && result.stdout?.trim()) {
2080
- msbuildInstalled = true;
2372
+ // `dotnet msbuild` evaluates the project (loading Directory.Build.rsp,
2373
+ // Directory.Build.props/targets and build extensions), so it belongs to
2374
+ // the dependency-tooling surface disabled by --no-install-deps and the
2375
+ // pre-build lifecycle. Version labels it would resolve then fall back to
2376
+ // the static props/central-version resolution.
2377
+ if (isProjectToolingAllowed(options)) {
2378
+ const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
2379
+ shell: isWin,
2380
+ });
2381
+ if (result.status === 0 && !result.error && result.stdout?.trim()) {
2382
+ msbuildInstalled = true;
2383
+ }
2081
2384
  }
2082
2385
  // `$(Name)` version labels resolve from the props files that can apply to
2083
2386
  // each project (beside and above it), read once for the whole scan.
@@ -2103,6 +2406,7 @@ export async function createCsharpBom(path, options) {
2103
2406
  getCentralPackageVersions(f, centralVersionsCache),
2104
2407
  agreedVersions,
2105
2408
  projectNames[projectDir] || {},
2409
+ restoredIdentities.get(resolve(f)) || undefined,
2106
2410
  );
2107
2411
  if (retMap?.parentComponent?.purl) {
2108
2412
  projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
@@ -2127,6 +2431,24 @@ export async function createCsharpBom(path, options) {
2127
2431
  parentComponent,
2128
2432
  );
2129
2433
  }
2434
+ // Only a version that needed msbuild makes its skip a loss
2435
+ if (
2436
+ retMap?.unresolvedVersionLabels?.length &&
2437
+ !isProjectToolingAllowed(options)
2438
+ ) {
2439
+ reportProjectToolingSkipped(
2440
+ options,
2441
+ "dotnet.msbuild.tooling-disabled",
2442
+ "Skipping 'dotnet msbuild' property evaluation since dependency installation is disabled. Package versions that come from MSBuild properties no project or props file sets stay empty.",
2443
+ {
2444
+ ecosystem: "csharp",
2445
+ tool: "msbuild",
2446
+ impact: "versions",
2447
+ detail:
2448
+ "Dependency installation is disabled, so $(Property) version references that the project and props files do not set were not evaluated with msbuild.",
2449
+ },
2450
+ );
2451
+ }
2130
2452
  }
2131
2453
  }
2132
2454
  // A lock file's or packages.config's direct dependencies belong to the
@@ -2148,6 +2470,17 @@ export async function createCsharpBom(path, options) {
2148
2470
  }
2149
2471
  }
2150
2472
  }
2473
+ // A scan of several projects is made of them, so the directory depends on
2474
+ // each, as a Gradle build does on its subprojects. Restore output also lists
2475
+ // every project it restored as a package; the parent and its projects are
2476
+ // already in the BOM, and a second component with the same bom-ref would
2477
+ // merge the two in every consumer.
2478
+ const projectRefs = new Set([parentComponent?.["bom-ref"]]);
2479
+ for (const project of parentComponent?.components || []) {
2480
+ parentDependsOn.add(project["bom-ref"]);
2481
+ projectRefs.add(project["bom-ref"]);
2482
+ }
2483
+ pkgList = pkgList.filter((p) => !projectRefs.has(p["bom-ref"]));
2151
2484
  if (pkgList.length) {
2152
2485
  pkgList = trimComponents(pkgList);
2153
2486
  // Perform deep analysis using dosai
@@ -2166,19 +2499,46 @@ export async function createCsharpBom(path, options) {
2166
2499
  ? resolve(path, options.depsSlicesFile)
2167
2500
  : join(slicesDir, "dosai.json");
2168
2501
  try {
2502
+ // A run that will run evinse (--evidence or --include-crypto, after
2503
+ // profiles are applied) keeps the report full: evinse reuses this
2504
+ // file as its usages slice and persists it as the native report.
2505
+ // Anything else needs only the sections cdxgen reads, so dosai
2506
+ // leaves the rest out (Metadata.OmittedSections names it).
2507
+ const willRunEvinse = runsEvidenceAnalysis(options);
2508
+ // Reuse an existing slice as-is, with one exception: a trimmed file
2509
+ // (an earlier --deep run left it behind) is regenerated full before
2510
+ // an evidence run uses it, so dosai still runs once per scan.
2511
+ let reuseExisting = safeExistsSync(slicesFile);
2512
+ if (reuseExisting && willRunEvinse) {
2513
+ const omitted = dosaiReportFileOmittedSections(slicesFile);
2514
+ if (omitted.length) {
2515
+ if (DEBUG_MODE) {
2516
+ console.log(
2517
+ `The existing dosai methods slice "${slicesFile}" omits sections (${omitted.join(", ")}); regenerating the full report because this run performs evidence analysis.`,
2518
+ );
2519
+ }
2520
+ reuseExisting = false;
2521
+ }
2522
+ }
2169
2523
  // Create the slices file if it doesn't exist
2170
- if (!safeExistsSync(slicesFile)) {
2524
+ if (!reuseExisting) {
2171
2525
  thoughtLog(
2172
2526
  "Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
2173
2527
  );
2174
2528
  const sliceResult = createDosaiMethodsSlice(
2175
2529
  resolve(path),
2176
2530
  resolve(slicesFile),
2177
- options,
2531
+ {
2532
+ ...options,
2533
+ dosaiMethodsSections: willRunEvinse
2534
+ ? undefined
2535
+ : dosaiMethodsSectionsArgument(),
2536
+ },
2178
2537
  );
2179
- // A stopped run has already said so and what to change; the debug
2180
- // output would show no dosai error for it (issue 4438).
2181
- if (!sliceResult && !dosaiRunWasStopped(resolve(slicesFile))) {
2538
+ // A stopped run, or one the .NET host refused to start, has
2539
+ // already said why and what to change; the debug output would show
2540
+ // no other dosai error for it (issue 4438).
2541
+ if (!sliceResult && !dosaiFailureReported(resolve(slicesFile))) {
2182
2542
  // Visible without debug output: otherwise the components silently
2183
2543
  // lose the dependency-slice evidence while the scan still exits 0.
2184
2544
  // --evidence (which implies --deep) runs dosai again during evinse
@@ -2205,6 +2565,11 @@ export async function createCsharpBom(path, options) {
2205
2565
  const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
2206
2566
  collectDosaiServicesFromMethods(methodsSlice, servicesMap);
2207
2567
  services = normalizeDosaiServiceMap(servicesMap);
2568
+ dosaiTools = dosaiToolComponents(
2569
+ slicesFile,
2570
+ methodsSlice,
2571
+ getPluginToolComponents(["dosai"])[0],
2572
+ );
2208
2573
  } finally {
2209
2574
  if (slicesDir) {
2210
2575
  safeRmSync(slicesDir, { recursive: true, force: true });
@@ -2244,7 +2609,7 @@ export async function createCsharpBom(path, options) {
2244
2609
  dependencies,
2245
2610
  parentComponent,
2246
2611
  services,
2247
- tools: options.deep ? getPluginToolComponents(["dosai"]) : [],
2612
+ tools: dosaiTools,
2248
2613
  });
2249
2614
  }
2250
2615
 
@@ -2263,58 +2628,239 @@ export async function createCryptoCertsBom(path, options) {
2263
2628
  },
2264
2629
  };
2265
2630
  }
2631
+ const collector = new CertificateCollector();
2266
2632
  const pkgList = [];
2267
- const certFiles = getAllFiles(
2268
- path,
2269
- `${
2270
- options.multiProject ? "**/" : ""
2271
- }*.{p12,jks,jceks,bks,keystore,key,pem,cer,gpg,pub}`,
2272
- options,
2633
+ // In rootfs/container mode the scan path sits inside the extracted tree;
2634
+ // locations are recorded relative to that tree so they merge with the
2635
+ // trustinspector rootfs producer (G18).
2636
+ const locationRoot = options.allLayersExplodedDir;
2637
+ const relativeLocation = (file) =>
2638
+ containerRelativePath(file, locationRoot) || file;
2639
+ // Bom-refs never carry host paths: outside rootfs mode, key and keystore
2640
+ // refs are relative to the scanned path.
2641
+ const refLocation = (file) =>
2642
+ containerRelativePath(file, locationRoot || path) || file;
2643
+ const certFiles = [
2644
+ ...new Set([
2645
+ ...getAllFiles(
2646
+ path,
2647
+ `${
2648
+ options.multiProject ? "**/" : ""
2649
+ }*.{p12,jks,jceks,bks,keystore,key,pem,cer,crt,der,p7b,p7c,pfx,truststore,gpg,pub}`,
2650
+ options,
2651
+ ),
2652
+ // Java truststores carry no extension; match them by exact name.
2653
+ ...getAllFiles(
2654
+ path,
2655
+ `${options.multiProject ? "**/" : ""}{cacerts,jssecacerts}`,
2656
+ options,
2657
+ ),
2658
+ ]),
2659
+ ];
2660
+ // Newlines would corrupt the plugin's stdin file list, and oversized
2661
+ // files are capped in the fallback; the plugin list gets the same caps.
2662
+ const pluginCandidateFiles = certFiles.filter(
2663
+ (file) =>
2664
+ !/[\r\n]/.test(file) &&
2665
+ statSync(file, { throwIfNoEntry: false })?.size <= MAX_CRYPTO_FILE_BYTES,
2273
2666
  );
2274
- const certHashes = await mapWithConcurrency(certFiles, (f) =>
2275
- checksumFile("sha256", f),
2667
+ const pluginMaterials = runTrustInspectorCertsMode(
2668
+ pluginCandidateFiles,
2669
+ locationRoot,
2276
2670
  );
2277
- for (let ci = 0; ci < certFiles.length; ci++) {
2278
- const f = certFiles[ci];
2279
- const fileHash = certHashes[ci];
2280
- const name = basename(f);
2281
- let evidence;
2282
- if (options.evidence) {
2283
- const identityEvidence = {
2284
- field: "name",
2285
- confidence: 1,
2286
- concludedValue: name,
2287
- methods: [
2671
+ const fallbackRecords = [];
2672
+ if (pluginMaterials) {
2673
+ collector.addTrustInspectorMaterials(pluginMaterials);
2674
+ for (const material of pluginMaterials) {
2675
+ if (typeof material?.path !== "string" || !material.path) {
2676
+ continue;
2677
+ }
2678
+ if (material.kind === "keystore") {
2679
+ pkgList.push(
2680
+ sourceKeystoreComponent(material, refLocation(material.path)),
2681
+ );
2682
+ } else if (material.kind === "private-key") {
2683
+ pkgList.push(
2684
+ sourcePrivateKeyComponent(material, refLocation(material.path)),
2685
+ );
2686
+ }
2687
+ }
2688
+ }
2689
+ // Files the plugin was not given (names with line breaks, oversized
2690
+ // files) are still read here, so no name can hide a file from the
2691
+ // inventory; without a plugin every file is. The files it was given are
2692
+ // its own, except for the keys it builds nothing from: it reports nothing
2693
+ // for a bare PEM public key, and an OpenPGP key only as public-key
2694
+ // material, so both are built here whether it saw them or not.
2695
+ const pluginFiles = new Set(pluginMaterials ? pluginCandidateFiles : []);
2696
+ for (const f of certFiles) {
2697
+ let bytes;
2698
+ try {
2699
+ const stats = statSync(f);
2700
+ if (stats.size > MAX_CRYPTO_FILE_BYTES) {
2701
+ continue;
2702
+ }
2703
+ bytes = readFileSync(f);
2704
+ } catch {
2705
+ continue;
2706
+ }
2707
+ const location = relativeLocation(f);
2708
+ const classification = classifyCryptoFile(bytes);
2709
+ if (
2710
+ pluginFiles.has(f) &&
2711
+ classification.kind !== "gpg" &&
2712
+ classification.kind !== "public-key"
2713
+ ) {
2714
+ continue;
2715
+ }
2716
+ if (classification.kind === "certificates") {
2717
+ for (const der of classification.ders) {
2718
+ const record = certRecordFromDer(der, {
2719
+ location,
2720
+ sourceType: deriveCryptoSourceType(f),
2721
+ });
2722
+ if (record) {
2723
+ record.fileExtension = extname(f).replace(/^\./, "") || undefined;
2724
+ fallbackRecords.push(record);
2725
+ }
2726
+ }
2727
+ } else if (classification.kind === "private-key") {
2728
+ pkgList.push(
2729
+ sourcePrivateKeyComponent(
2288
2730
  {
2289
- technique: "filename",
2290
- confidence: 1,
2291
- value: f,
2731
+ format: classification.format,
2732
+ sourceType: deriveCryptoSourceType(f),
2292
2733
  },
2293
- ],
2294
- };
2295
- evidence = {
2296
- identity:
2297
- options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
2298
- occurrences: [{ location: f }],
2299
- };
2734
+ refLocation(f),
2735
+ ),
2736
+ );
2737
+ } else if (classification.kind === "gpg") {
2738
+ pkgList.push(await sourceGpgComponent(f, location, options));
2739
+ } else if (classification.kind === "public-key") {
2740
+ pkgList.push(await sourcePemPublicKeyComponent(f, location, options));
2741
+ } else if (classification.kind === "keystore") {
2742
+ pkgList.push(
2743
+ sourceKeystoreComponent(
2744
+ {
2745
+ format: classification.format,
2746
+ sourceType: deriveCryptoSourceType(f),
2747
+ },
2748
+ refLocation(f),
2749
+ ),
2750
+ );
2751
+ }
2752
+ }
2753
+ // Kubernetes TLS secrets and Android SPKI pins are cdxgen-side producers:
2754
+ // the plugin has no YAML or Android-config parser.
2755
+ const yamlFiles = getAllFiles(
2756
+ path,
2757
+ `${options.multiProject ? "**/" : ""}*.{yaml,yml}`,
2758
+ options,
2759
+ );
2760
+ for (const yamlFile of yamlFiles) {
2761
+ let yamlText;
2762
+ try {
2763
+ const stats = statSync(yamlFile);
2764
+ if (stats.size > MAX_CRYPTO_FILE_BYTES) {
2765
+ continue;
2766
+ }
2767
+ yamlText = readFileSync(yamlFile, "utf-8");
2768
+ } catch {
2769
+ continue;
2300
2770
  }
2301
- const apkg = {
2302
- name,
2303
- type: "cryptographic-asset",
2304
- version: fileHash,
2305
- "bom-ref": `crypto/certificate/${name}@sha256:${fileHash}`,
2306
- cryptoProperties: {
2307
- assetType: "certificate",
2308
- algorithmProperties: {
2309
- executionEnvironment: "unknown",
2310
- implementationPlatform: "unknown",
2771
+ // Cheap pre-filter: the Secret type string must be present before the
2772
+ // YAML parser runs.
2773
+ if (!yamlText.includes("kubernetes.io/tls")) {
2774
+ continue;
2775
+ }
2776
+ for (const record of kubernetesTlsSecretRecords(yamlText, {
2777
+ file: relativeLocation(yamlFile),
2778
+ })) {
2779
+ fallbackRecords.push(record);
2780
+ }
2781
+ }
2782
+ const androidXmlFiles = getAllFiles(path, "**/res/xml/*.xml", options);
2783
+ for (const xmlFile of androidXmlFiles) {
2784
+ let xmlText;
2785
+ try {
2786
+ const stats = statSync(xmlFile, { throwIfNoEntry: false });
2787
+ if (!stats || stats.size > MAX_CRYPTO_FILE_BYTES) {
2788
+ continue;
2789
+ }
2790
+ xmlText = readFileSync(xmlFile, "utf-8");
2791
+ } catch {
2792
+ continue;
2793
+ }
2794
+ if (!xmlText.includes("<network-security-config")) {
2795
+ continue;
2796
+ }
2797
+ for (const pin of androidNetworkSecurityConfigPins(xmlText, {
2798
+ file: relativeLocation(xmlFile),
2799
+ })) {
2800
+ collector.addPin(pin);
2801
+ }
2802
+ }
2803
+ if (fallbackRecords.length) {
2804
+ linkFallbackIssuers(fallbackRecords);
2805
+ collector.addRecords(fallbackRecords);
2806
+ }
2807
+ const {
2808
+ components: certificateComponents,
2809
+ dependencies: certificateDependencies,
2810
+ } = collector.build();
2811
+ if (pluginMaterials) {
2812
+ // The plugin reports the certificates a readable keystore holds, not the
2813
+ // keystore itself; give it the same component the fallback emits and
2814
+ // link it to what it holds.
2815
+ const keystoreMaterials = new Map();
2816
+ for (const material of pluginMaterials) {
2817
+ if (material?.kind === "certificate" && material.path) {
2818
+ keystoreMaterials.set(material.path, material);
2819
+ }
2820
+ }
2821
+ for (const file of pluginCandidateFiles) {
2822
+ const location = relativeLocation(file);
2823
+ const material = keystoreMaterials.get(location);
2824
+ if (!material) {
2825
+ continue;
2826
+ }
2827
+ let classification;
2828
+ try {
2829
+ classification = classifyCryptoFile(readFileSync(file));
2830
+ } catch {
2831
+ continue;
2832
+ }
2833
+ if (classification.kind !== "keystore") {
2834
+ continue;
2835
+ }
2836
+ const keystore = sourceKeystoreComponent(
2837
+ {
2838
+ format: classification.format,
2839
+ sourceType: material.sourceType || deriveCryptoSourceType(file),
2840
+ readable: true,
2311
2841
  },
2312
- },
2313
- ...(evidence ? { evidence } : {}),
2314
- properties: [{ name: "internal:SrcFile", value: f }],
2315
- };
2316
- pkgList.push(apkg);
2842
+ refLocation(file),
2843
+ );
2844
+ pkgList.push(keystore);
2845
+ const held = certificateComponents
2846
+ .filter(
2847
+ (component) =>
2848
+ component.cryptoProperties?.assetType === "certificate" &&
2849
+ component.evidence?.occurrences?.some(
2850
+ (occurrence) => occurrence.location === location,
2851
+ ),
2852
+ )
2853
+ .map((component) => component["bom-ref"])
2854
+ .sort();
2855
+ if (held.length) {
2856
+ certificateDependencies.push({
2857
+ ref: keystore["bom-ref"],
2858
+ dependsOn: held,
2859
+ });
2860
+ }
2861
+ }
2317
2862
  }
2863
+ pkgList.push(...certificateComponents);
2318
2864
  const sourceCryptoComponents = await collectSourceCryptoComponents(
2319
2865
  path,
2320
2866
  options,
@@ -2335,9 +2881,229 @@ export async function createCryptoCertsBom(path, options) {
2335
2881
  bomJson: {
2336
2882
  components: filterInvalidCryptoComponents(pkgList),
2337
2883
  },
2884
+ dependencies: certificateDependencies,
2885
+ };
2886
+ }
2887
+
2888
+ // Files larger than this are not classified: no real certificate bundle or
2889
+ // keystore comes close, and the cap bounds hostile inputs.
2890
+ const MAX_CRYPTO_FILE_BYTES = 16 * 1024 * 1024;
2891
+
2892
+ function deriveCryptoSourceType(filePath) {
2893
+ const normalized = String(filePath).replace(/\\/g, "/");
2894
+ if (
2895
+ normalized.includes("/etc/ssl/certs") ||
2896
+ normalized.includes("ca-certificates")
2897
+ ) {
2898
+ return "ca-store";
2899
+ }
2900
+ if (normalized.includes("certifi") || normalized.includes("botocore")) {
2901
+ return "language-bundle";
2902
+ }
2903
+ if (
2904
+ /\/etc\/(nginx|apache2|httpd|haproxy|letsencrypt|kubernetes|docker|containerd)\//.test(
2905
+ normalized,
2906
+ )
2907
+ ) {
2908
+ return "server-config";
2909
+ }
2910
+ if (
2911
+ normalized.includes("cacerts") ||
2912
+ /\.(jks|jceks|p12|pfx|truststore)$/i.test(normalized)
2913
+ ) {
2914
+ return "java-truststore";
2915
+ }
2916
+ return "generic";
2917
+ }
2918
+
2919
+ function sourceCryptoFileEvidence(location, options) {
2920
+ if (!options.evidence) {
2921
+ return undefined;
2922
+ }
2923
+ const identityEvidence = {
2924
+ field: "name",
2925
+ confidence: 1,
2926
+ concludedValue: basename(location),
2927
+ methods: [
2928
+ {
2929
+ technique: "filename",
2930
+ confidence: 1,
2931
+ value: location,
2932
+ },
2933
+ ],
2934
+ };
2935
+ return {
2936
+ identity:
2937
+ options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
2938
+ occurrences: [{ location }],
2939
+ };
2940
+ }
2941
+
2942
+ async function sourceGpgComponent(srcFile, location, options) {
2943
+ const fileHash = await checksumFile("sha256", srcFile);
2944
+ return {
2945
+ name: basename(location),
2946
+ type: "cryptographic-asset",
2947
+ version: fileHash,
2948
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
2949
+ cryptoProperties: {
2950
+ assetType: "related-crypto-material",
2951
+ relatedCryptoMaterialProperties: {
2952
+ type: "public-key",
2953
+ format: "OpenPGP",
2954
+ },
2955
+ },
2956
+ ...(sourceCryptoFileEvidence(location, options)
2957
+ ? { evidence: sourceCryptoFileEvidence(location, options) }
2958
+ : {}),
2959
+ properties: [
2960
+ { name: "internal:SrcFile", value: location },
2961
+ { name: "cdx:crypto:sourceType", value: "generic" },
2962
+ ],
2963
+ };
2964
+ }
2965
+
2966
+ async function sourcePemPublicKeyComponent(srcFile, location, options) {
2967
+ const fileHash = await checksumFile("sha256", srcFile);
2968
+ return {
2969
+ name: basename(location),
2970
+ type: "cryptographic-asset",
2971
+ version: fileHash,
2972
+ "bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
2973
+ cryptoProperties: {
2974
+ assetType: "related-crypto-material",
2975
+ relatedCryptoMaterialProperties: {
2976
+ type: "public-key",
2977
+ format: "SPKI",
2978
+ },
2979
+ },
2980
+ ...(sourceCryptoFileEvidence(location, options)
2981
+ ? { evidence: sourceCryptoFileEvidence(location, options) }
2982
+ : {}),
2983
+ properties: [
2984
+ { name: "internal:SrcFile", value: location },
2985
+ { name: "cdx:crypto:sourceType", value: "generic" },
2986
+ ],
2338
2987
  };
2339
2988
  }
2340
2989
 
2990
+ function sourcePrivateKeyComponent(material, location) {
2991
+ return {
2992
+ name: basename(location),
2993
+ type: "cryptographic-asset",
2994
+ "bom-ref": `crypto/related-crypto-material/private-key/${encodeURIComponent(location)}`,
2995
+ cryptoProperties: {
2996
+ assetType: "related-crypto-material",
2997
+ relatedCryptoMaterialProperties: {
2998
+ type: "private-key",
2999
+ ...(material?.format ? { format: material.format } : {}),
3000
+ },
3001
+ },
3002
+ evidence: { occurrences: [{ location }] },
3003
+ properties: [
3004
+ { name: "internal:SrcFile", value: location },
3005
+ ...(material?.sourceType
3006
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
3007
+ : []),
3008
+ ],
3009
+ };
3010
+ }
3011
+
3012
+ function sourceKeystoreComponent(material, location) {
3013
+ const readable = (material?.properties || []).find(
3014
+ (property) => property.name === "cdx:crypto:keystoreReadable",
3015
+ )?.value;
3016
+ return {
3017
+ name: basename(location),
3018
+ type: "cryptographic-asset",
3019
+ "bom-ref": `crypto/related-crypto-material/keystore/${encodeURIComponent(location)}`,
3020
+ cryptoProperties: {
3021
+ assetType: "related-crypto-material",
3022
+ relatedCryptoMaterialProperties: {
3023
+ type: "other",
3024
+ ...(material?.format ? { format: material.format } : {}),
3025
+ },
3026
+ },
3027
+ evidence: { occurrences: [{ location }] },
3028
+ properties: [
3029
+ { name: "internal:SrcFile", value: location },
3030
+ ...(material?.sourceType
3031
+ ? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
3032
+ : []),
3033
+ // A keystore the plugin read has no keystoreReadable property.
3034
+ ...(material?.readable
3035
+ ? []
3036
+ : [
3037
+ { name: "cdx:crypto:keystoreReadable", value: readable || "false" },
3038
+ ]),
3039
+ ],
3040
+ };
3041
+ }
3042
+
3043
+ /**
3044
+ * Parse the candidate certificate files with the trustinspector `certs`
3045
+ * mode. Only a report-2 envelope is accepted: report-1 output has no
3046
+ * certificate identity to build the shared model from, and the Node
3047
+ * fallback covers that case.
3048
+ *
3049
+ * @param {string[]} files candidate files
3050
+ * @param {string} root paths are reported relative to this directory
3051
+ * @returns {object[]|undefined} materials, or undefined when the plugin is
3052
+ * absent, blocked or not report-2
3053
+ */
3054
+ function runTrustInspectorCertsMode(files, root) {
3055
+ if (!files?.length) {
3056
+ return undefined;
3057
+ }
3058
+ if (isDryRun) {
3059
+ recordActivity({
3060
+ kind: "trustinspector",
3061
+ reason:
3062
+ "Dry run mode blocks trustinspector execution and reports the requested inspection instead.",
3063
+ status: "blocked",
3064
+ target: `certs --stdin (${files.length} files)`,
3065
+ });
3066
+ return undefined;
3067
+ }
3068
+ const trustInspectorBin = resolvePluginBinary("trustinspector");
3069
+ if (!trustInspectorBin) {
3070
+ return undefined;
3071
+ }
3072
+ // `--root` is passed only in rootfs/container mode, where locations must
3073
+ // come out relative to the extracted tree; a plain source scan keeps the
3074
+ // host paths every other producer records. The value reaches a spawned
3075
+ // command, so it gets the same rejection the rootfs path applies:
3076
+ // dangerous unicode or line breaks disable the plugin call.
3077
+ const args = ["certs", "--stdin"];
3078
+ const safeRoot =
3079
+ typeof root === "string" &&
3080
+ !hasDangerousUnicode(root) &&
3081
+ !/[\r\n]/.test(root)
3082
+ ? root
3083
+ : undefined;
3084
+ if (safeRoot) {
3085
+ args.push("--root", safeRoot);
3086
+ }
3087
+ const result = safeSpawnSync(trustInspectorBin, args, {
3088
+ input: `${files.join("\n")}\n`,
3089
+ maxBuffer: 256 * 1024 * 1024,
3090
+ // A hung plugin must not hang cdxgen; the Node fallback takes over.
3091
+ timeout: 10 * 60 * 1000,
3092
+ });
3093
+ if (result?.status !== 0 || result?.error || !result?.stdout) {
3094
+ return undefined;
3095
+ }
3096
+ try {
3097
+ const report = JSON.parse(result.stdout);
3098
+ if (report?.schema !== "trustinspector/report-2") {
3099
+ return undefined;
3100
+ }
3101
+ return report.materials || [];
3102
+ } catch {
3103
+ return undefined;
3104
+ }
3105
+ }
3106
+
2341
3107
  /**
2342
3108
  * Function to create bom string for R projects using renv.
2343
3109
  *