@cdxgen/cdxgen 13.1.0 → 13.3.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 (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -20,6 +20,17 @@ try {
20
20
  SaferExec = undefined;
21
21
  }
22
22
 
23
+ /**
24
+ * Whether the @cdxgen/safer-exec tracing runtime could be loaded. When false,
25
+ * dynamic tracing cannot run at all, and every trace would otherwise exit 0
26
+ * with a valid but empty BOM, hiding the broken installation (#4378).
27
+ *
28
+ * @returns {boolean} True when the tracing runtime is available
29
+ */
30
+ export function isSaferExecAvailable() {
31
+ return Boolean(SaferExec);
32
+ }
33
+
23
34
  const __filename = fileURLToPath(import.meta.url);
24
35
  const __dirname = dirname(__filename);
25
36
 
@@ -105,8 +116,10 @@ export function resolveSaferExecBinary() {
105
116
  }
106
117
 
107
118
  for (const modulesDir of searchDirs) {
108
- // Direct structure under node_modules
109
- const directPath = join(modulesDir, pkgName, "bin", "safer-exec");
119
+ // Direct structure under node_modules. The Go runtime inside the
120
+ // platform package is bin/safer-exec-rt; "safer-exec" is the JS
121
+ // dispatcher's npm bin name, not a file on disk.
122
+ const directPath = join(modulesDir, pkgName, "bin", "safer-exec-rt");
110
123
  let realDirectPath;
111
124
  try {
112
125
  realDirectPath = realpathSync(directPath);
@@ -157,7 +170,19 @@ export function resolveSaferExecBinary() {
157
170
  * @param {boolean} [options.traceExec] - Log every child process spawned
158
171
  * @param {string[]} [options.allowExec] - Executables the command is allowed to run
159
172
  * @param {string[]} [options.blockExec] - Executables to block from running
160
- * @returns {Promise<{libPaths: string[], httpAccessEntries: Object[]}>} Collected libraries and HTTP URLs
173
+ * @param {boolean} [options.proxyEgress] - Route egress through safer-exec's hostname-pinning proxy (network stays enabled; only allowHosts are reachable)
174
+ * @param {boolean} [options.allowLoopback] - Allow loopback connections
175
+ * @param {boolean} [options.dryRun] - Deny every filesystem and network side effect and collect a dry-run report
176
+ * @param {string} [options.policy] - Named safer-exec ecosystem policy (npm, pypi, maven, ...)
177
+ * @param {string} [options.policyFile] - Path to a safer-exec JSON policy file
178
+ * @param {boolean} [options.blockInterpreters] - Block interpreters with sandbox or task-port exemptions (macOS)
179
+ * @param {boolean} [options.denyPersistenceWrites] - Deny writes to auto-execution and persistence locations
180
+ * @param {boolean} [options.privateTmp] - Private tmpfs /tmp and /var/tmp (Linux)
181
+ * @param {string} [options.protectHome] - Home isolation: read-only, tmpfs, or off (Linux)
182
+ * @returns {Promise<{libPaths: string[], httpAccessEntries: Object[], egressEntries?: Object[], dryRunSummary?: Object}>} Collected libraries, HTTP URLs, proxy egress decisions, and the dry-run summary
183
+ * @throws {Error} When the @cdxgen/safer-exec tracing runtime is not installed
184
+ * (for example after pruning it from a standalone payload), since the traced
185
+ * command could never run and the BOM would silently be empty
161
186
  */
162
187
  export async function executeAndTrace(commandStr, workingDir, options = {}) {
163
188
  const emptyResult = { libPaths: [], httpAccessEntries: [] };
@@ -174,11 +199,23 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
174
199
  );
175
200
 
176
201
  if (!SaferExec) {
177
- return emptyResult;
202
+ throw new Error(
203
+ "Dynamic tracing requires the @cdxgen/safer-exec package, which could not be loaded from this installation. Re-install with optional dependencies enabled, or use the official tracebom standalone binary.",
204
+ );
205
+ }
206
+
207
+ const exec = new SaferExec();
208
+ // Policies are applied first so explicit options below take precedence, and
209
+ // outside the try so an unknown policy or unreadable policy file fails the
210
+ // run instead of producing an empty BOM.
211
+ if (options.policy) {
212
+ exec.applyPolicy(options.policy);
213
+ }
214
+ if (options.policyFile) {
215
+ exec.applyPolicyFile(options.policyFile);
178
216
  }
179
217
 
180
218
  try {
181
- const exec = new SaferExec();
182
219
  if (workingDir) {
183
220
  exec.workingDir(workingDir);
184
221
  }
@@ -192,9 +229,41 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
192
229
  exec.traceLibraries().suppressLibLoadStderr(true).enableAudit();
193
230
 
194
231
  // Apply sandbox options
195
- if (options.disableNetwork !== false && !options.traceHTTPURLs) {
232
+ // The egress proxy runs on the host side and must be reachable, so it
233
+ // needs the network enabled (an isolated namespace would bypass it).
234
+ if (
235
+ options.disableNetwork !== false &&
236
+ !options.traceHTTPURLs &&
237
+ !options.proxyEgress
238
+ ) {
196
239
  exec.disableNetwork();
197
240
  }
241
+ if (options.proxyEgress) {
242
+ if (!options.allowHosts?.length && !options.allowUrls?.length) {
243
+ console.warn(
244
+ "[cdxgen trace] egress proxy enabled without --allow-host: every outbound connection will be denied.",
245
+ );
246
+ }
247
+ exec.proxyEgress();
248
+ }
249
+ if (options.allowLoopback) {
250
+ exec.allowLoopback();
251
+ }
252
+ if (options.dryRun) {
253
+ exec.enableDryRun();
254
+ }
255
+ if (options.blockInterpreters) {
256
+ exec.blockInterpreters();
257
+ }
258
+ if (options.denyPersistenceWrites) {
259
+ exec.denyPersistenceWrites();
260
+ }
261
+ if (options.privateTmp) {
262
+ exec.privateTmp();
263
+ }
264
+ if (options.protectHome && options.protectHome !== "off") {
265
+ exec.protectHome(options.protectHome);
266
+ }
198
267
  if (options.readPaths?.length) {
199
268
  exec.readPaths(options.readPaths);
200
269
  }
@@ -306,25 +375,53 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
306
375
  }
307
376
 
308
377
  const result = await exec.run(cmd, args);
309
- if (result && result.exitCode !== 0 && result.stderr) {
310
- if (result.stderr.includes("[safer-exec] Error:")) {
311
- console.error(
312
- "Tracing launcher execution failed:",
313
- result.stderr.trim(),
378
+ const stderr = result?.stderr || "";
379
+ if (result && result.exitCode !== 0) {
380
+ if (stderr.includes("[safer-exec] Error:")) {
381
+ console.error("Tracing launcher execution failed:", stderr.trim());
382
+ } else {
383
+ // A failing command usually means a partial trace (for example a
384
+ // sandbox denial), so the BOM may be incomplete.
385
+ console.warn(
386
+ `[cdxgen trace] traced command exited with code ${result.exitCode}; the BOM may be incomplete.`,
314
387
  );
315
388
  }
316
- } else if (options.traceHTTPURLs && result?.stderr) {
317
- // Surface eBPF/http-trace warnings even when exit code is 0
318
- const stderr = result.stderr || "";
389
+ }
390
+ if (options.traceHTTPURLs) {
391
+ // eBPF URL tracing needs CAP_BPF/CAP_PERFMON. Surface why it was not
392
+ // available whatever the command's exit code, since the BOM otherwise
393
+ // silently lacks services.
394
+ const httpLines = stderr
395
+ .split("\n")
396
+ .filter(
397
+ (line) =>
398
+ line.includes("http-trace") ||
399
+ line.includes("httptrace") ||
400
+ line.includes("SSL/TLS libraries"),
401
+ );
319
402
  if (
320
- stderr.includes("http-trace") ||
321
- stderr.includes("httptrace") ||
322
- stderr.includes("SSL/TLS libraries")
403
+ httpLines.some((line) => /warning|not supported|failed/i.test(line))
323
404
  ) {
324
- console.warn("[cdxgen trace] HTTP URL tracing warning:", stderr.trim());
405
+ console.warn(
406
+ "[cdxgen trace] HTTP URL tracing unavailable or degraded (it needs root or CAP_BPF and CAP_PERFMON):",
407
+ httpLines.join("\n"),
408
+ );
325
409
  }
326
410
  }
327
411
 
412
+ // Without an event source (strace on Linux) a dry run still denies every
413
+ // side effect but reports nothing, which must not read as "no activity".
414
+ let dryRunSummary = result?.dryRun?.summary;
415
+ if (
416
+ options.dryRun &&
417
+ result?.stderr?.includes("dry-run cannot capture attempted operations")
418
+ ) {
419
+ console.warn(
420
+ "[cdxgen trace] dry run could not capture attempted operations on this host (install strace or relax ptrace_scope); the BOM records no counts.",
421
+ );
422
+ dryRunSummary = { captured: false };
423
+ }
424
+
328
425
  // Read and parse CBOM file if temp/explicit path was used
329
426
  let cryptoComponents = [];
330
427
  const cbomPathToRead = options.cbom || tempCbomPath;
@@ -377,6 +474,8 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
377
474
  httpAccessEntries: [...collectedUrls, ...urls],
378
475
  cryptoEntries: [...collectedCrypto, ...cryptoLogs],
379
476
  cryptoComponents,
477
+ egressEntries: parseEgressEntries(result.auditLog),
478
+ dryRunSummary,
380
479
  };
381
480
  }
382
481
 
@@ -385,6 +484,8 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
385
484
  httpAccessEntries: collectedUrls,
386
485
  cryptoEntries: collectedCrypto,
387
486
  cryptoComponents,
487
+ egressEntries: [],
488
+ dryRunSummary,
388
489
  };
389
490
  } catch (err) {
390
491
  console.error("Tracing command execution failed:", err);
@@ -392,6 +493,86 @@ export async function executeAndTrace(commandStr, workingDir, options = {}) {
392
493
  return emptyResult;
393
494
  }
394
495
 
496
+ /**
497
+ * Returns the sorted, de-duplicated parameter names of a query string.
498
+ *
499
+ * @param {string} query - Raw query string, with or without a leading "?"
500
+ * @returns {string} Comma-separated parameter names, or "" when there are none
501
+ */
502
+ function queryParameterNames(query) {
503
+ if (!query) {
504
+ return "";
505
+ }
506
+ const names = new Set();
507
+ for (const pair of String(query).replace(/^\?/, "").split("&")) {
508
+ const eqIdx = pair.indexOf("=");
509
+ const name = (eqIdx >= 0 ? pair.substring(0, eqIdx) : pair).trim();
510
+ if (name) {
511
+ names.add(name);
512
+ }
513
+ }
514
+ return Array.from(names).sort().join(",");
515
+ }
516
+
517
+ /**
518
+ * Extracts the egress proxy's decisions from a safer-exec audit log.
519
+ * "proxy-connect" targets are "host:port"; "proxy-violation" targets are
520
+ * "CONNECT host:port" or "METHOD http://host[:port]/path". Only the authority
521
+ * is kept, never a path or query.
522
+ *
523
+ * @param {Object[]} auditLog - safer-exec audit entries
524
+ * @returns {{host: string, port: number, protocol: string, decision: string}[]} Unique egress decisions
525
+ */
526
+ export function parseEgressEntries(auditLog) {
527
+ const seen = new Set();
528
+ const entries = [];
529
+ for (const e of auditLog || []) {
530
+ if (e?.type !== "proxy-connect" && e?.type !== "proxy-violation") {
531
+ continue;
532
+ }
533
+ const target = String(e.target || "");
534
+ let authority = target;
535
+ let protocol = "https";
536
+ if (e.type === "proxy-connect") {
537
+ protocol = e.details === "http" ? "http" : "https";
538
+ } else {
539
+ const spaceIdx = target.indexOf(" ");
540
+ authority = spaceIdx >= 0 ? target.substring(spaceIdx + 1) : target;
541
+ const schemeIdx = authority.indexOf("://");
542
+ if (schemeIdx >= 0) {
543
+ protocol = authority.substring(0, schemeIdx);
544
+ authority = authority.substring(schemeIdx + 3);
545
+ const slashIdx = authority.indexOf("/");
546
+ if (slashIdx >= 0) {
547
+ authority = authority.substring(0, slashIdx);
548
+ }
549
+ }
550
+ }
551
+ let host = authority;
552
+ let port = protocol === "http" ? 80 : 443;
553
+ const colonIdx = authority.lastIndexOf(":");
554
+ if (colonIdx > 0 && !authority.endsWith("]")) {
555
+ const parsedPort = Number.parseInt(authority.substring(colonIdx + 1), 10);
556
+ if (!Number.isNaN(parsedPort)) {
557
+ port = parsedPort;
558
+ host = authority.substring(0, colonIdx);
559
+ }
560
+ }
561
+ host = host.replace(/^\[|\]$/g, "").toLowerCase();
562
+ if (!host) {
563
+ continue;
564
+ }
565
+ const decision = e.type === "proxy-connect" ? "allowed" : "denied";
566
+ const key = `${decision}|${protocol}|${host}|${port}`;
567
+ if (seen.has(key)) {
568
+ continue;
569
+ }
570
+ seen.add(key);
571
+ entries.push({ host, port, protocol, decision });
572
+ }
573
+ return entries;
574
+ }
575
+
395
576
  /**
396
577
  * Groups HTTP access entries into a CycloneDX services-ready map.
397
578
  * Each unique (host, port, protocol) combination becomes a service.
@@ -409,7 +590,9 @@ export function groupHttpEntriesToServices(httpAccessEntries) {
409
590
  properties: [],
410
591
  };
411
592
  }
412
- const endpoint = `https://${entry.host}${entry.port && entry.port !== 443 ? `:${entry.port}` : ""}${entry.path || "/"}`;
593
+ const scheme = entry.protocol === "http" ? "http" : "https";
594
+ const defaultPort = scheme === "http" ? 80 : 443;
595
+ const endpoint = `${scheme}://${entry.host}${entry.port && entry.port !== defaultPort ? `:${entry.port}` : ""}${entry.path || "/"}`;
413
596
  servicesMap[serviceName].endpoints.add(endpoint);
414
597
  if (entry.method) {
415
598
  const methodProp = {
@@ -424,8 +607,14 @@ export function groupHttpEntriesToServices(httpAccessEntries) {
424
607
  servicesMap[serviceName].properties.push(methodProp);
425
608
  }
426
609
  }
427
- if (entry.query) {
428
- const queryProp = { name: "cdx:dynamic:httpQuery", value: entry.query };
610
+ // Query values can carry tokens and signatures, so only the parameter
611
+ // names are recorded.
612
+ const queryParams = queryParameterNames(entry.query);
613
+ if (queryParams) {
614
+ const queryProp = {
615
+ name: "cdx:dynamic:httpQueryParams",
616
+ value: queryParams,
617
+ };
429
618
  if (
430
619
  !servicesMap[serviceName].properties.some(
431
620
  (p) => p.name === queryProp.name && p.value === queryProp.value,
@@ -7,6 +7,7 @@ import {
7
7
  } from "node:fs";
8
8
  import { platform as _platform, homedir } from "node:os";
9
9
  import { basename, dirname, extname, join, relative, resolve } from "node:path";
10
+ import process from "node:process";
10
11
 
11
12
  import { build, Purl } from "@cdxgen/cdx-purl";
12
13
 
@@ -54,7 +55,11 @@ import {
54
55
  resolvePluginBinary,
55
56
  setPluginsPathEnv,
56
57
  } from "../inventory/plugins.js";
57
- import { genericPurl, tryBuildPurl } from "../inventory/purl.js";
58
+ import {
59
+ genericPurl,
60
+ parsePurlLoose,
61
+ tryBuildPurl,
62
+ } from "../inventory/purl.js";
58
63
  import {
59
64
  adjustLicenseInformation,
60
65
  findLicenseId,
@@ -531,28 +536,89 @@ export function getCargoAuditableInfo(src) {
531
536
  return undefined;
532
537
  }
533
538
 
539
+ /**
540
+ * Check that sourcekitten (bundled plugin or SOURCEKITTEN_CMD) answers a
541
+ * trivial syntax request.
542
+ *
543
+ * @param env {Object} Optional environment overrides merged over process.env
544
+ * @returns {String} `ok`; `unavailable` when there is no bundled plugin and
545
+ * SOURCEKITTEN_CMD is unset; `not-found` when the configured executable does
546
+ * not exist; `failed` when it ran but could not answer
547
+ */
548
+ export function probeSourcekitten(env) {
549
+ if (!SOURCEKITTEN_BIN) {
550
+ return "unavailable";
551
+ }
552
+ const result = safeSpawnSync(
553
+ SOURCEKITTEN_BIN,
554
+ ["syntax", "--text", "import Swift"],
555
+ env ? { env: { ...process.env, ...env } } : undefined,
556
+ );
557
+ if (result?.error?.code === "ENOENT") {
558
+ return "not-found";
559
+ }
560
+ if (result?.status === 0 && result.stdout) {
561
+ try {
562
+ JSON.parse(result.stdout);
563
+ return "ok";
564
+ } catch (_e) {
565
+ // Plain-text errors on stdout are failures
566
+ }
567
+ }
568
+ if (DEBUG_MODE) {
569
+ console.log(
570
+ "sourcekitten syntax probe failed:",
571
+ `${result?.stderr || result?.error || ""}`
572
+ .split("\n")
573
+ .slice(0, 5)
574
+ .map((l) => l.slice(0, 400))
575
+ .join("\n"),
576
+ );
577
+ }
578
+ return "failed";
579
+ }
580
+
534
581
  /**
535
582
  * Execute sourcekitten plugin with the given arguments
536
583
  *
537
584
  * @param args {Array} Arguments
585
+ * @param env {Object} Optional environment overrides merged over process.env,
586
+ * used to align sourcekitten with a specific Swift toolchain
538
587
  * @returns {undefined|Object} Command output
539
588
  */
540
- export function executeSourcekitten(args) {
541
- if (SOURCEKITTEN_BIN) {
542
- const result = safeSpawnSync(SOURCEKITTEN_BIN, args);
543
- if (result.status !== 0 || result.error) {
544
- if (result.stdout || result.stderr) {
545
- console.error(result.stdout, result.stderr);
546
- }
547
- }
548
- if (result) {
549
- const stdout = result.stdout;
550
- if (stdout) {
551
- return JSON.parse(stdout);
552
- }
589
+ export function executeSourcekitten(args, env) {
590
+ if (!SOURCEKITTEN_BIN) {
591
+ return undefined;
592
+ }
593
+ const result = safeSpawnSync(
594
+ SOURCEKITTEN_BIN,
595
+ args,
596
+ env ? { env: { ...process.env, ...env } } : undefined,
597
+ );
598
+ if ((result.status !== 0 || result.error) && DEBUG_MODE) {
599
+ // Failures echo whole clang invocations, so only the first diagnostic
600
+ // lines are worth showing
601
+ const diagnostics = `${result.stderr || ""}`.split("\n").slice(0, 5);
602
+ console.log(
603
+ `sourcekitten ${args?.[0]} failed:`,
604
+ diagnostics.map((l) => l.slice(0, 400)).join("\n"),
605
+ );
606
+ }
607
+ if (!result?.stdout) {
608
+ return undefined;
609
+ }
610
+ try {
611
+ return JSON.parse(result.stdout);
612
+ } catch (_e) {
613
+ // Oversized outputs are truncated at the spawn buffer limit, and
614
+ // sourcekitten prints plain-text errors to stdout on some failures
615
+ if (DEBUG_MODE) {
616
+ console.log(
617
+ `Ignoring unparseable sourcekitten ${args?.[0]} output of ${result.stdout.length} bytes.`,
618
+ );
553
619
  }
620
+ return undefined;
554
621
  }
555
- return undefined;
556
622
  }
557
623
 
558
624
  /**
@@ -796,8 +862,16 @@ export async function getOSPackages(src, imageConfig, options = {}) {
796
862
  );
797
863
  }
798
864
  }
799
- if (TRIVY_BIN) {
800
- let imageType = "image";
865
+ // trivy-cdxgen only scans a root filesystem; it has no `image` command. The
866
+ // caller hands over the exported image directory, so a missing path means
867
+ // the export failed and there is nothing for trivy to scan.
868
+ const rootfsPresent = TRIVY_BIN ? safeExistsSync(src) : false;
869
+ if (TRIVY_BIN && !rootfsPresent) {
870
+ console.warn(
871
+ `Skipping the trivy OS package scan: the root filesystem ${src} does not exist.`,
872
+ );
873
+ }
874
+ if (TRIVY_BIN && rootfsPresent) {
801
875
  const trivyCacheDir = join(homedir(), ".cache", "trivy");
802
876
  try {
803
877
  safeMkdirSync(join(trivyCacheDir, "db"), { recursive: true });
@@ -805,13 +879,10 @@ export async function getOSPackages(src, imageConfig, options = {}) {
805
879
  } catch (_err) {
806
880
  // ignore errors
807
881
  }
808
- if (safeExistsSync(src)) {
809
- imageType = "rootfs";
810
- }
811
882
  const tempDir = safeMkdtempSync(join(getTmpDir(), "trivy-cdxgen-"));
812
883
  const bomJsonFile = join(tempDir, "trivy-bom.json");
813
884
  const args = [
814
- imageType,
885
+ "rootfs",
815
886
  "--cache-dir",
816
887
  trivyCacheDir,
817
888
  "--output",
@@ -932,7 +1003,7 @@ export async function getOSPackages(src, imageConfig, options = {}) {
932
1003
  ? tmpBom.metadata.tools
933
1004
  : tmpBom?.metadata?.tools?.components || []
934
1005
  ).filter((tool) => tool?.["bom-ref"] && tool?.name !== "cdxgen"),
935
- ["trivy"].concat(imageType === "rootfs" ? ["trustinspector"] : []),
1006
+ ["trivy", "trustinspector"],
936
1007
  );
937
1008
  const toolRefs = extractToolRefs(
938
1009
  { components: tools },
@@ -2977,7 +3048,20 @@ function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
2977
3048
  }
2978
3049
  }
2979
3050
 
2980
- const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
3051
+ /**
3052
+ * Collect the dependency edges trivy recorded for one OS package component.
3053
+ *
3054
+ * Trivy keys its `dependencies` by its own bom-refs, which may leave the `+` of
3055
+ * an rpm or deb version unescaped (`...libstdc%2B%2B6@14.2.0+git10526-...`), so
3056
+ * each ref is read structurally and re-rendered in cdxgen's decoded bom-ref
3057
+ * form with the component's type, namespace and distro qualifiers.
3058
+ *
3059
+ * @param {Object<string, string[]>} tmpDependencies Trivy's refs mapped to the refs they depend on
3060
+ * @param {string} origBomRef The component's bom-ref as trivy wrote it
3061
+ * @param {object} comp The component after cdxgen rewrote its purl and bom-ref
3062
+ * @returns {{ref: string, dependsOn: string[]}|undefined} Dependency entry for the component
3063
+ */
3064
+ export const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
2981
3065
  try {
2982
3066
  const tmpDependsOn =
2983
3067
  tmpDependencies[origBomRef] || tmpDependencies[comp["bom-ref"]] || [];
@@ -2985,7 +3069,14 @@ const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
2985
3069
  tmpDependsOn.forEach((d) => {
2986
3070
  try {
2987
3071
  const compPurl = Purl.parse(comp.purl);
2988
- const tmpPurl = Purl.parse(d.replace("none", compPurl.type));
3072
+ // Trivy's dependency refs are its bom-refs, which can leave the `+` of
3073
+ // an rpm or deb version unescaped; read them structurally so the edge
3074
+ // survives.
3075
+ const depParts = parsePurlLoose(d.replace("none", compPurl.type));
3076
+ if (!depParts) {
3077
+ return;
3078
+ }
3079
+ const tmpPurl = new Purl(depParts);
2989
3080
  tmpPurl.type = compPurl.type;
2990
3081
  // FIXME: Check if this hack is still needed with the latest trivy
2991
3082
  if (OS_PURL_TYPES.includes(compPurl.type)) {