@cdxgen/cdxgen 13.4.0 → 13.5.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 (208) hide show
  1. package/README.md +7 -7
  2. package/bin/cdxgen-secure.js +15 -0
  3. package/bin/cdxgen.js +25 -5
  4. package/bin/evinse.js +5 -0
  5. package/bin/hbom.js +15 -7
  6. package/bin/repl.js +29 -0
  7. package/data/cbomosdb-queries.json +13 -1
  8. package/data/component-tags.json +8 -0
  9. package/data/queries-darwin.json +25 -1
  10. package/data/queries-win.json +19 -1
  11. package/data/queries.json +41 -0
  12. package/data/remediations.json +283 -1
  13. package/data/rules/cbom-security.yaml +235 -4
  14. package/data/rules/go-binaries.yaml +46 -0
  15. package/data/rules/golem-go.yaml +80 -0
  16. package/data/rules/hbom-compliance.yaml +40 -22
  17. package/data/rules/hbom-performance.yaml +7 -1
  18. package/data/rules/host-topology.yaml +1 -1
  19. package/data/rules/obom-runtime.yaml +31 -2
  20. package/data/rules/php-runtime.yaml +143 -0
  21. package/lib/audit/index.js +14 -0
  22. package/lib/cli/bomAssembly.js +28 -3
  23. package/lib/cli/cliOptions.js +110 -3
  24. package/lib/cli/index.js +73 -4
  25. package/lib/cli/jvmBom.js +126 -20
  26. package/lib/cli/managedBom.js +711 -71
  27. package/lib/cli/nativeBom.js +225 -28
  28. package/lib/core/env.js +30 -8
  29. package/lib/core/fs.js +28 -19
  30. package/lib/core/httpClient.js +74 -45
  31. package/lib/core/projectConfig.js +1 -0
  32. package/lib/core/projectTooling.js +97 -0
  33. package/lib/core/propertySanitizer.js +72 -0
  34. package/lib/ecosystems/golem.js +550 -6
  35. package/lib/ecosystems/gradleutils.js +30 -20
  36. package/lib/ecosystems/parsers-dotnet.js +117 -18
  37. package/lib/ecosystems/parsers-go.js +1090 -367
  38. package/lib/ecosystems/parsers-jvm.js +259 -17
  39. package/lib/ecosystems/parsers-misc.js +229 -44
  40. package/lib/ecosystems/parsers-python.js +11 -10
  41. package/lib/ecosystems/rubyutils.js +6 -3
  42. package/lib/ecosystems/rusi.js +10 -0
  43. package/lib/ecosystems/scalacliutils.js +2 -1
  44. package/lib/ecosystems/scalasem.js +3 -1
  45. package/lib/evinser/evinser.js +126 -18
  46. package/lib/evinser/swiftsem.js +68 -5
  47. package/lib/inventory/agentSkillSpec.js +17 -0
  48. package/lib/inventory/apkInstalledDb.js +243 -0
  49. package/lib/inventory/atomUtils.js +33 -0
  50. package/lib/inventory/auditCategories.js +3 -3
  51. package/lib/inventory/cacheDir.js +52 -9
  52. package/lib/inventory/certificates.js +2371 -0
  53. package/lib/inventory/communityAiConfigParser.js +4 -9
  54. package/lib/inventory/deps.js +6 -4
  55. package/lib/inventory/display.js +200 -3
  56. package/lib/inventory/dosai.js +500 -58
  57. package/lib/inventory/dynamic.js +45 -3
  58. package/lib/inventory/envcontext.js +168 -5
  59. package/lib/inventory/evidenceUtils.js +436 -23
  60. package/lib/inventory/formulationParsers.js +3 -2
  61. package/lib/inventory/goBuildInfo.js +140 -0
  62. package/lib/inventory/hbom.js +67 -7
  63. package/lib/inventory/hbomAnalysis.js +16 -0
  64. package/lib/inventory/hostTopology.js +21 -0
  65. package/lib/inventory/osPackagePurl.js +244 -0
  66. package/lib/inventory/osPackageResolver.js +89 -87
  67. package/lib/inventory/osinfo.js +79 -31
  68. package/lib/inventory/osqueryTransform.js +114 -5
  69. package/lib/inventory/phpBuildInfo.js +487 -0
  70. package/lib/inventory/phpHostProbe.js +641 -0
  71. package/lib/inventory/rootfsCerts.js +426 -0
  72. package/lib/inventory/source.js +207 -3
  73. package/lib/inventory/spdx.js +144 -0
  74. package/lib/inventory/traceRunner.js +5 -1
  75. package/lib/inventory/tracedCrypto.js +129 -0
  76. package/lib/managers/binary.js +529 -183
  77. package/lib/managers/docker.js +166 -12
  78. package/lib/parsers/email.js +277 -0
  79. package/lib/parsers/largeJson.js +34 -2
  80. package/lib/server/server.js +186 -1
  81. package/lib/stages/postgen/annotator.js +17 -1
  82. package/lib/stages/postgen/introspection/reflect.js +4 -1
  83. package/lib/stages/postgen/postgen.js +5 -5
  84. package/lib/stages/postgen/ruleEngine.js +58 -1
  85. package/lib/stages/postgen/specVersionCompat.js +30 -0
  86. package/lib/stages/pregen/pregen.js +117 -5
  87. package/lib/validator/bomValidator.js +26 -5
  88. package/lib/validator/generated/runtime.mjs +13 -0
  89. package/lib/validator/generated/validate-1.6.mjs +26 -7
  90. package/lib/validator/generated/validate-1.7.mjs +31 -12
  91. package/lib/validator/generated/validate-2.0.mjs +30 -11
  92. package/package.json +17 -17
  93. package/types/bin/cdxgen-secure.d.ts +3 -0
  94. package/types/bin/cdxgen-secure.d.ts.map +1 -0
  95. package/types/bin/repl.d.ts.map +1 -1
  96. package/types/lib/audit/index.d.ts.map +1 -1
  97. package/types/lib/cli/bomAssembly.d.ts +9 -2
  98. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  99. package/types/lib/cli/cliOptions.d.ts +43 -0
  100. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  101. package/types/lib/cli/index.d.ts.map +1 -1
  102. package/types/lib/cli/jvmBom.d.ts +20 -0
  103. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  104. package/types/lib/cli/managedBom.d.ts.map +1 -1
  105. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  106. package/types/lib/core/env.d.ts +18 -2
  107. package/types/lib/core/env.d.ts.map +1 -1
  108. package/types/lib/core/fs.d.ts.map +1 -1
  109. package/types/lib/core/httpClient.d.ts.map +1 -1
  110. package/types/lib/core/projectConfig.d.ts.map +1 -1
  111. package/types/lib/core/projectTooling.d.ts +41 -0
  112. package/types/lib/core/projectTooling.d.ts.map +1 -0
  113. package/types/lib/core/propertySanitizer.d.ts +26 -0
  114. package/types/lib/core/propertySanitizer.d.ts.map +1 -1
  115. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  116. package/types/lib/ecosystems/golem.d.ts +81 -1
  117. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  118. package/types/lib/ecosystems/gradleutils.d.ts +10 -8
  119. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  120. package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
  121. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  122. package/types/lib/ecosystems/parsers-go.d.ts +85 -13
  123. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  124. package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
  125. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  126. package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
  127. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  128. package/types/lib/ecosystems/parsers-python.d.ts +4 -3
  129. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  130. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  131. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  132. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  133. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
  134. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  135. package/types/lib/evinser/evinser.d.ts +25 -0
  136. package/types/lib/evinser/evinser.d.ts.map +1 -1
  137. package/types/lib/evinser/swiftsem.d.ts +10 -0
  138. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  139. package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
  140. package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
  141. package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
  142. package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
  143. package/types/lib/inventory/atomUtils.d.ts +14 -0
  144. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  145. package/types/lib/inventory/cacheDir.d.ts +12 -4
  146. package/types/lib/inventory/cacheDir.d.ts.map +1 -1
  147. package/types/lib/inventory/certificates.d.ts +205 -0
  148. package/types/lib/inventory/certificates.d.ts.map +1 -0
  149. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  150. package/types/lib/inventory/deps.d.ts +2 -1
  151. package/types/lib/inventory/deps.d.ts.map +1 -1
  152. package/types/lib/inventory/display.d.ts +18 -0
  153. package/types/lib/inventory/display.d.ts.map +1 -1
  154. package/types/lib/inventory/dosai.d.ts +145 -6
  155. package/types/lib/inventory/dosai.d.ts.map +1 -1
  156. package/types/lib/inventory/dynamic.d.ts +6 -1
  157. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  158. package/types/lib/inventory/envcontext.d.ts +40 -2
  159. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  160. package/types/lib/inventory/evidenceUtils.d.ts +29 -10
  161. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  162. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  163. package/types/lib/inventory/goBuildInfo.d.ts +10 -0
  164. package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
  165. package/types/lib/inventory/hbom.d.ts.map +1 -1
  166. package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
  167. package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
  168. package/types/lib/inventory/hostTopology.d.ts.map +1 -1
  169. package/types/lib/inventory/osPackagePurl.d.ts +99 -0
  170. package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
  171. package/types/lib/inventory/osPackageResolver.d.ts +6 -2
  172. package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
  173. package/types/lib/inventory/osinfo.d.ts +31 -6
  174. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  175. package/types/lib/inventory/osqueryTransform.d.ts +51 -0
  176. package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
  177. package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
  178. package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
  179. package/types/lib/inventory/phpHostProbe.d.ts +35 -0
  180. package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
  181. package/types/lib/inventory/rootfsCerts.d.ts +27 -0
  182. package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
  183. package/types/lib/inventory/source.d.ts +28 -0
  184. package/types/lib/inventory/source.d.ts.map +1 -1
  185. package/types/lib/inventory/spdx.d.ts +15 -0
  186. package/types/lib/inventory/spdx.d.ts.map +1 -1
  187. package/types/lib/inventory/traceRunner.d.ts +4 -1
  188. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  189. package/types/lib/inventory/tracedCrypto.d.ts +22 -0
  190. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
  191. package/types/lib/managers/binary.d.ts +10 -0
  192. package/types/lib/managers/binary.d.ts.map +1 -1
  193. package/types/lib/managers/docker.d.ts +12 -0
  194. package/types/lib/managers/docker.d.ts.map +1 -1
  195. package/types/lib/parsers/email.d.ts +19 -0
  196. package/types/lib/parsers/email.d.ts.map +1 -0
  197. package/types/lib/parsers/largeJson.d.ts +13 -0
  198. package/types/lib/parsers/largeJson.d.ts.map +1 -1
  199. package/types/lib/server/server.d.ts +8 -0
  200. package/types/lib/server/server.d.ts.map +1 -1
  201. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  202. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
  203. package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
  204. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  205. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  206. package/types/lib/stages/pregen/pregen.d.ts +14 -1
  207. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  208. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -14,7 +14,9 @@ import {
14
14
  isSecureMode,
15
15
  readEnvironmentVariable,
16
16
  } from "../core/activity.js";
17
+ import { PROJECT_TYPE_ALIASES } from "../core/env.js";
17
18
  import { isWin } from "../core/paths.js";
19
+ import { isSecureModeActive } from "../core/projectTooling.js";
18
20
  import { resetRunState } from "../core/runState.js";
19
21
  import { CDXGEN_VERSION } from "../core/state.js";
20
22
  import { installConsoleShim, ui } from "../core/ui.js";
@@ -27,6 +29,7 @@ import {
27
29
  getGitAllowProtocol,
28
30
  gitClone,
29
31
  isAllowedPath,
32
+ isAllowedRealPath,
30
33
  isAllowedWinPath,
31
34
  maybePurlSource,
32
35
  maybeRemotePath,
@@ -82,6 +85,11 @@ const ALLOWED_PARAMS = [
82
85
  "tlpClassification",
83
86
  ];
84
87
 
88
+ // Options that install packages or run the scanned project's own build tools
89
+ // (for example `cargo check`, which runs build.rs), and so execute code from
90
+ // the scanned repository.
91
+ const CODE_EXECUTION_PARAMS = ["installDeps", "deep"];
92
+
85
93
  const app = connect();
86
94
 
87
95
  export { isAllowedHttpHost };
@@ -156,6 +164,7 @@ export function parseValue(raw) {
156
164
  * @returns {Object} Populated options object
157
165
  */
158
166
  export function parseQueryString(q, body = {}, options = {}) {
167
+ const operatorOptions = { ...options };
159
168
  // Priority is query params followed by body
160
169
  for (const param of ALLOWED_PARAMS) {
161
170
  const raw = q[param] ?? body[param];
@@ -163,7 +172,7 @@ export function parseQueryString(q, body = {}, options = {}) {
163
172
  options[param] = parseValue(raw);
164
173
  }
165
174
  }
166
- options.projectType = options.type?.split(",");
175
+ options.projectType = toProjectTypeList(options.type);
167
176
  delete options.type;
168
177
  if (options.lifecycle === "pre-build") {
169
178
  options.installDeps = false;
@@ -171,9 +180,170 @@ export function parseQueryString(q, body = {}, options = {}) {
171
180
  if (options.profile) {
172
181
  applyProfileOptions(options);
173
182
  }
183
+ // failOnError ends the process, so one failing request would stop the server
184
+ // for every client.
185
+ options.failOnError = false;
186
+ restrictRequestOptions(options, operatorOptions);
174
187
  return options;
175
188
  }
176
189
 
190
+ /**
191
+ * Normalise a `type` value from the request or the start-up options, which may
192
+ * be a comma separated string or an array, into a list of project types.
193
+ *
194
+ * @param {string|string[]|undefined} type Project type value
195
+ * @returns {string[]|undefined} Project types, or undefined when none are given
196
+ */
197
+ function toProjectTypeList(type) {
198
+ if (type === undefined || type === null || type === "") {
199
+ return undefined;
200
+ }
201
+ const types = (Array.isArray(type) ? type : [type])
202
+ .flatMap((t) => String(t).split(","))
203
+ .map((t) => t.trim())
204
+ .filter(Boolean);
205
+ return types.length ? types : undefined;
206
+ }
207
+
208
+ /**
209
+ * Stop a request from turning on options that execute code from the scanned
210
+ * repository when the operator has not. In secure mode, or when the server was
211
+ * started with --no-install-deps, a request may turn these options off but
212
+ * never on.
213
+ *
214
+ * @param {Object} options Options parsed from the request
215
+ * @param {Object} operatorOptions Options the server was started with
216
+ * @returns {void}
217
+ */
218
+ function restrictRequestOptions(options, operatorOptions) {
219
+ if (!isSecureModeActive() && operatorOptions.installDeps !== false) {
220
+ return;
221
+ }
222
+ for (const param of CODE_EXECUTION_PARAMS) {
223
+ // Only an explicit false from the request changes the operator's choice.
224
+ // Values such as "" or 0 would otherwise pass gates that test `!== false`.
225
+ options[param] =
226
+ operatorOptions[param] === true && options[param] !== false;
227
+ }
228
+ }
229
+
230
+ // Project types that inventory the host the server runs on, such as installed
231
+ // packages, users, shell history, IDE and browser extensions, or the Maven,
232
+ // Gradle, sbt, Cargo and Helm caches in the server user's home directory.
233
+ // They ignore the scan path, so the path allowlist does not limit them.
234
+ const HOST_SCOPE_PROJECT_TYPES = new Set(
235
+ [
236
+ "os",
237
+ "hbom",
238
+ "cargo-cache",
239
+ "helm-index",
240
+ "gradle-index",
241
+ "sbt-index",
242
+ "maven-index",
243
+ "vscode-extension",
244
+ "chrome-extension",
245
+ "agentic",
246
+ ]
247
+ .flatMap((key) => PROJECT_TYPE_ALIASES[key] || [key])
248
+ .concat(["maven-repo"]),
249
+ );
250
+
251
+ /**
252
+ * Check request options that the server must refuse outright.
253
+ *
254
+ * @param {Object} reqOptions Options parsed from the request
255
+ * @param {Object} operatorOptions Options the server was started with
256
+ * @returns {Object|null} Error object with status, error and details, or null
257
+ */
258
+ export function validateRequestOptions(reqOptions, operatorOptions = {}) {
259
+ const operatorTypes = new Set(
260
+ toProjectTypeList(operatorOptions.type ?? operatorOptions.projectType) ||
261
+ [],
262
+ );
263
+ const hostScopeType = (reqOptions.projectType || []).find(
264
+ (t) => HOST_SCOPE_PROJECT_TYPES.has(t) && !operatorTypes.has(t),
265
+ );
266
+ if (hostScopeType) {
267
+ return {
268
+ status: 403,
269
+ error: "Project Type Not Allowed",
270
+ details: `The '${hostScopeType}' project type scans the server host. Start the server with this type to allow it.`,
271
+ };
272
+ }
273
+ const excludes = Array.isArray(reqOptions.exclude)
274
+ ? reqOptions.exclude
275
+ : [reqOptions.exclude];
276
+ // Brace patterns expand before matching, so one short pattern such as
277
+ // {1..100000} can keep the server busy for minutes.
278
+ if (
279
+ reqOptions.exclude !== operatorOptions.exclude &&
280
+ excludes.some(
281
+ (e) => typeof e === "string" && (e.includes("{") || e.length > 512),
282
+ )
283
+ ) {
284
+ return {
285
+ status: 400,
286
+ error: "Invalid Exclude Pattern",
287
+ details:
288
+ "Exclude patterns sent to the server cannot use braces or exceed 512 characters.",
289
+ };
290
+ }
291
+ if (
292
+ reqOptions.serverUrl &&
293
+ reqOptions.serverUrl !== operatorOptions.serverUrl
294
+ ) {
295
+ const requestedOrigin = urlOrigin(reqOptions.serverUrl);
296
+ if (!requestedOrigin) {
297
+ return {
298
+ status: 400,
299
+ error: "Invalid Server URL",
300
+ details: "The Dependency-Track server URL is invalid.",
301
+ };
302
+ }
303
+ if (requestedOrigin !== urlOrigin(operatorOptions.serverUrl)) {
304
+ // The operator's API key only ever goes to the operator's server.
305
+ if (
306
+ operatorOptions.apiKey &&
307
+ reqOptions.apiKey === operatorOptions.apiKey
308
+ ) {
309
+ return {
310
+ status: 400,
311
+ error: "API Key Required",
312
+ details:
313
+ "A request that names its own Dependency-Track server must send its own API key.",
314
+ };
315
+ }
316
+ // CDXGEN_SERVER_ALLOWED_HOSTS only covers git hosts, so in secure mode a
317
+ // server the operator did not configure must be on CDXGEN_ALLOWED_HOSTS.
318
+ if (isSecureMode && !readEnvironmentVariable("CDXGEN_ALLOWED_HOSTS")) {
319
+ return {
320
+ status: 403,
321
+ error: "Host Not Allowed",
322
+ details:
323
+ "Set CDXGEN_ALLOWED_HOSTS to let requests choose a Dependency-Track server.",
324
+ };
325
+ }
326
+ }
327
+ }
328
+ if (isSecureMode && reqOptions.includeFormulation && reqOptions.serverUrl) {
329
+ return {
330
+ status: 400,
331
+ error: "Formulation Not Allowed",
332
+ details:
333
+ "Formulation data may contain secrets and is not submitted to Dependency-Track in secure mode.",
334
+ };
335
+ }
336
+ return null;
337
+ }
338
+
339
+ function urlOrigin(value) {
340
+ try {
341
+ return new URL(value).origin;
342
+ } catch (_err) {
343
+ return undefined;
344
+ }
345
+ }
346
+
177
347
  /**
178
348
  * Extracts query parameters from an incoming HTTP request object.
179
349
  * Handles repeated keys by collecting their values into an array.
@@ -391,6 +561,18 @@ const start = (options) => {
391
561
  }),
392
562
  );
393
563
  }
564
+ const optionsError = validateRequestOptions(reqOptions, options);
565
+ if (optionsError) {
566
+ res.writeHead(optionsError.status, {
567
+ "Content-Type": "application/json",
568
+ });
569
+ return res.end(
570
+ JSON.stringify({
571
+ error: optionsError.error,
572
+ details: optionsError.details,
573
+ }),
574
+ );
575
+ }
394
576
  // Enforce the v13 CycloneDX spec floor on the HTTP entry point too, so the
395
577
  // server cannot be used to request a generation target the CLI rejects.
396
578
  if (reqOptions.specVersion !== undefined) {
@@ -502,8 +684,11 @@ const start = (options) => {
502
684
  cleanup = true;
503
685
  } else {
504
686
  srcDir = sourcePath;
687
+ // Check where a symlinked path really points as well, so a link inside
688
+ // an allowed directory cannot open a scan of a directory outside it.
505
689
  if (
506
690
  !isAllowedPath(path.resolve(srcDir)) ||
691
+ !isAllowedRealPath(srcDir) ||
507
692
  (isWin && !isAllowedWinPath(srcDir))
508
693
  ) {
509
694
  res.writeHead(403, { "Content-Type": "application/json" });
@@ -463,6 +463,15 @@ export function textualMetadata(bomJson) {
463
463
  const swidCount = bomJson?.components?.filter((c) =>
464
464
  c?.purl?.startsWith("pkg:swid"),
465
465
  ).length;
466
+ // Homebrew packages are pkg:brew, not pkg:swid, so they are not counted as
467
+ // applications; formulae and casks are told apart by a property.
468
+ const brewComponents =
469
+ bomJson?.components?.filter((c) => c?.purl?.startsWith("pkg:brew/")) || [];
470
+ const brewCaskCount = brewComponents.filter((c) =>
471
+ c.properties?.some(
472
+ (p) => p.name === "cdx:homebrew:type" && p.value === "cask",
473
+ ),
474
+ ).length;
466
475
  const { unpackagedExecutableCount, unpackagedSharedLibraryCount } =
467
476
  getContainerFileInventoryStats(bomJson?.components);
468
477
  const githubStats = getGitHubWorkflowStats(bomJson?.components);
@@ -711,7 +720,14 @@ export function textualMetadata(bomJson) {
711
720
  text = `${text} The system appears to be set up for remote development, with ${vsixCount} Visual Studio Code extensions installed.`;
712
721
  }
713
722
  if (swidCount > 0) {
714
- text = `${text} In addition, there are ${swidCount} applications installed on the system.`;
723
+ text = `${text} In addition, there ${swidCount === 1 ? "is 1 application" : `are ${swidCount} applications`} installed on the system.`;
724
+ }
725
+ if (brewComponents.length > 0) {
726
+ const brewCount = brewComponents.length;
727
+ const casks = brewCaskCount
728
+ ? `, of which ${brewCaskCount} ${brewCaskCount === 1 ? "is a cask" : "are casks"}`
729
+ : "";
730
+ text = `${text} The system also has ${brewCount} Homebrew package${brewCount === 1 ? "" : "s"} installed${casks}.`;
715
731
  }
716
732
  }
717
733
  if (bomType === "HBOM" && hbomSummary) {
@@ -36,6 +36,7 @@ import {
36
36
  isVersionedJvmToolProjectType,
37
37
  PROJECT_TYPE_ALIASES,
38
38
  } from "../../../core/env.js";
39
+ import { safeExistsSync } from "../../../core/fs.js";
39
40
  import { dirNameStr } from "../../../core/paths.js";
40
41
  import { CDXGEN_VERSION } from "../../../core/state.js";
41
42
  import { evaluateRules, loadRules } from "../ruleEngine.js";
@@ -389,7 +390,9 @@ function resolveLedgerEvents(context) {
389
390
  };
390
391
  }
391
392
  const sidecarPath = readEnvironmentVariable("CDXGEN_INTROSPECT_LEDGER");
392
- if (sidecarPath) {
393
+ // A sidecar the run could not create (the permission model denies writes
394
+ // outside the granted paths) holds nothing; the buffer still has the events.
395
+ if (sidecarPath && safeExistsSync(sidecarPath)) {
393
396
  return { events: loadLedgerFile(sidecarPath), source: "sidecar" };
394
397
  }
395
398
  const events = getLedgerEvents();
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { basename, join, relative, sep } from "node:path";
2
+ import { join, relative } from "node:path";
3
3
  import process from "node:process";
4
4
 
5
5
  import { Purl } from "@cdxgen/cdx-purl";
@@ -943,10 +943,10 @@ function applyCustomMetadataProperties(bomJson, options) {
943
943
  export function applyStandards(bomJson, options) {
944
944
  if (options.standard && Array.isArray(options.standard)) {
945
945
  for (let astandard of options.standard) {
946
- // See issue: #1953
947
- if (astandard.includes(sep)) {
948
- astandard = basename(astandard);
949
- }
946
+ // See issue: #1953. Server requests can send either separator, and on
947
+ // Windows both "/" and "\" separate path segments, so keep only the
948
+ // last segment of either kind.
949
+ astandard = String(astandard).split(/[\\/]/).pop();
950
950
  const templateFile = join(
951
951
  dirNameStr,
952
952
  "data",
@@ -165,7 +165,18 @@ function hasProperty(obj, propName, expectedValue) {
165
165
  if (expectedValue === undefined) {
166
166
  return value !== null;
167
167
  }
168
- return value === String(expectedValue);
168
+ // A repeatable property (cdx:crypto:sourceType on a certificate found by
169
+ // several producers) matches when any of its values does.
170
+ return extractPropertyValues(obj, propName).includes(String(expectedValue));
171
+ }
172
+
173
+ function extractPropertyValues(obj, propName) {
174
+ if (!Array.isArray(obj?.properties)) {
175
+ return [];
176
+ }
177
+ return obj.properties
178
+ .filter((p) => p?.name === propName && p.value !== undefined)
179
+ .map((p) => String(p.value));
169
180
  }
170
181
 
171
182
  /**
@@ -315,6 +326,9 @@ function registerCdxHelpers(expression, helpers = {}) {
315
326
  expression.registerFunction("prop", (obj, propName) =>
316
327
  extractProperty(obj, propName),
317
328
  );
329
+ expression.registerFunction("propValues", (obj, propName) =>
330
+ extractPropertyValues(obj, propName),
331
+ );
318
332
  expression.registerFunction("nullSafeProp", (obj, propName) => {
319
333
  const value = extractProperty(obj, propName);
320
334
  return value === null ? "" : value;
@@ -385,6 +399,13 @@ function registerCdxHelpers(expression, helpers = {}) {
385
399
  expression.registerFunction("safeStr", (val) => {
386
400
  return val === null || val === undefined ? "" : String(val).trim();
387
401
  });
402
+ // End of validity in epoch milliseconds. A date-only value (2026-01-31)
403
+ // is valid through the end of that day in UTC; a full RFC 3339 timestamp
404
+ // is taken as is; anything else, or an impossible date, gives no value so
405
+ // the rule skips the component.
406
+ expression.registerFunction("certEndMillis", (val) =>
407
+ parseCertificateEndMillis(val),
408
+ );
388
409
  expression.registerFunction("parseSizeBytes", (val) => {
389
410
  if (val === null || val === undefined || val === "") {
390
411
  return null;
@@ -519,6 +540,42 @@ function registerCdxHelpers(expression, helpers = {}) {
519
540
  * @param {string} rulesDir - Path to directory containing .yaml rule files
520
541
  * @returns {Promise<Array>} Array of parsed rule objects
521
542
  */
543
+ const FULL_TIMESTAMP_RE =
544
+ /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?(Z|[+-]([01]\d|2[0-3]):[0-5]\d)$/;
545
+ const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
546
+
547
+ /**
548
+ * Parse a certificate notValidAfter value.
549
+ *
550
+ * @param {*} value timestamp or date-only string
551
+ * @returns {number|undefined} epoch milliseconds, or undefined when the value
552
+ * does not parse
553
+ */
554
+ export function parseCertificateEndMillis(value) {
555
+ if (typeof value !== "string") {
556
+ return undefined;
557
+ }
558
+ const text = value.trim();
559
+ const dateOnly = DATE_ONLY_RE.exec(text);
560
+ if (dateOnly) {
561
+ const [year, month, day] = dateOnly.slice(1).map(Number);
562
+ const start = new Date(Date.UTC(year, month - 1, day));
563
+ if (
564
+ start.getUTCFullYear() !== year ||
565
+ start.getUTCMonth() !== month - 1 ||
566
+ start.getUTCDate() !== day
567
+ ) {
568
+ return undefined;
569
+ }
570
+ return start.getTime() + 24 * 60 * 60 * 1000 - 1;
571
+ }
572
+ if (!FULL_TIMESTAMP_RE.test(text)) {
573
+ return undefined;
574
+ }
575
+ const millis = Date.parse(text);
576
+ return Number.isNaN(millis) ? undefined : millis;
577
+ }
578
+
522
579
  export async function loadRules(rulesDir) {
523
580
  const rules = [];
524
581
  if (!safeExistsSync(rulesDir)) {
@@ -604,9 +604,39 @@ function downgradeSubjectForSpecVersion(subject, specVersion, parentKey) {
604
604
  certificateProperties.certificateExtension =
605
605
  certificateProperties.certificateFileExtension;
606
606
  }
607
+ // `relatedCryptographicAssets` and `certificateExtensions` are 1.7-only;
608
+ // the deprecated `subjectPublicKeyRef`/`signatureAlgorithmRef` (valid in
609
+ // 1.6, see data/bom-1.6.schema.json) carry the same links, and the
610
+ // extension facts survive as `cdx:crypto:` properties.
611
+ const relatedAssets = certificateProperties.relatedCryptographicAssets;
612
+ if (Array.isArray(relatedAssets)) {
613
+ for (const asset of relatedAssets) {
614
+ if (
615
+ asset?.type === "publicKey" &&
616
+ !certificateProperties.subjectPublicKeyRef
617
+ ) {
618
+ certificateProperties.subjectPublicKeyRef = asset.ref;
619
+ } else if (
620
+ asset?.type === "algorithm" &&
621
+ !certificateProperties.signatureAlgorithmRef
622
+ ) {
623
+ certificateProperties.signatureAlgorithmRef = asset.ref;
624
+ }
625
+ }
626
+ delete certificateProperties.relatedCryptographicAssets;
627
+ }
628
+ delete certificateProperties.certificateExtensions;
607
629
  delete certificateProperties.serialNumber;
608
630
  delete certificateProperties.certificateFileExtension;
609
631
  delete certificateProperties.fingerprint;
632
+ } else if (
633
+ specVersion < 1.7 &&
634
+ subject.cryptoProperties?.assetType === "related-crypto-material" &&
635
+ subject.cryptoProperties.relatedCryptoMaterialProperties
636
+ ) {
637
+ // The public-key `fingerprint` object is 1.7-only; the SPKI digest stays
638
+ // available as the `cdx:crypto:spkiSha256` property.
639
+ delete subject.cryptoProperties.relatedCryptoMaterialProperties.fingerprint;
610
640
  }
611
641
  if (specVersion < 1.7 && subject.cryptoProperties?.algorithmProperties) {
612
642
  // `algorithmFamily` and `ellipticCurve` are CycloneDX 1.7 additions to
@@ -28,6 +28,13 @@ import {
28
28
  TIMEOUT_MS,
29
29
  } from "../../core/fs.js";
30
30
  import { isMac, isWin } from "../../core/paths.js";
31
+ import { isProjectToolingAllowed } from "../../core/projectTooling.js";
32
+ import {
33
+ dosaiDotnetRequirement,
34
+ dosaiDotnetRequirementAdvice,
35
+ dosaiDotnetRequirementDetail,
36
+ markDosaiLaunchFailureReported,
37
+ } from "../../inventory/dosai.js";
31
38
  import {
32
39
  bundleInstallWithDocker,
33
40
  collectJavaInfo,
@@ -60,6 +67,7 @@ import {
60
67
  readToolVersionOverride,
61
68
  resolvePartialVersion,
62
69
  } from "../../inventory/jvmToolEnv.js";
70
+ import { resolvePluginBinary } from "../../inventory/plugins.js";
63
71
  import {
64
72
  classifyProbeResult,
65
73
  describeSpawnRestriction,
@@ -74,12 +82,20 @@ import {
74
82
  * @param {Object} options CLI options
75
83
  */
76
84
  export function prepareEnv(filePath, options) {
77
- if (!options.projectType || isSecureMode) {
85
+ if (isSecureMode) {
78
86
  return;
79
87
  }
80
88
  if (filePath) {
81
89
  filePath = resolve(filePath);
82
90
  }
91
+ // Runs before the project type check: a .NET tree is also found when cdxgen
92
+ // detects the project types itself.
93
+ if (!isDryRun) {
94
+ prepareDotnetEnv(filePath, options);
95
+ }
96
+ if (!options.projectType) {
97
+ return;
98
+ }
83
99
  // The JVM preparation provisions the JDK ahead of the build tools, so an
84
100
  // alias it already installed must not be installed a second time here.
85
101
  // It runs under dry-run too, where it reports the toolchain it would set up
@@ -104,6 +120,91 @@ export function prepareEnv(filePath, options) {
104
120
  prepareRubyEnv(filePath, options);
105
121
  }
106
122
 
123
+ /**
124
+ * Check that dosai, which deep runs use for .NET evidence, can start. The
125
+ * dosai bundled with cdxgen-plugins-bin is framework-dependent, so it needs a
126
+ * .NET runtime at least as new as the one it targets (.NET 11 from dosai
127
+ * 5.0.0). Running it lets the .NET host apply its own version rules. Skipped
128
+ * in the container images, which ship a dosai that works with their SDK, and
129
+ * when DOSAI_CMD names another build, such as the self-contained -full one.
130
+ *
131
+ * @param {String} filePath Project path
132
+ * @param {Object} options CLI options
133
+ */
134
+ export function prepareDotnetEnv(filePath, options) {
135
+ if (
136
+ !options.deep ||
137
+ !filePath ||
138
+ readEnvironmentVariable("CDXGEN_IN_CONTAINER") === "true" ||
139
+ readEnvironmentVariable("DOSAI_CMD") ||
140
+ !isDotnetInScope(filePath, options)
141
+ ) {
142
+ return;
143
+ }
144
+ const dosaiBin = resolvePluginBinary("dosai");
145
+ if (!dosaiBin) {
146
+ // A missing plugin is reported where dosai runs
147
+ return;
148
+ }
149
+ const result = safeSpawnSync(dosaiBin, ["--version"], {
150
+ encoding: "utf-8",
151
+ timeout: TIMEOUT_MS,
152
+ });
153
+ if (result?.status === 0 && !result.error) {
154
+ return;
155
+ }
156
+ const output = commandOutputText(result) || "";
157
+ const requirement = dosaiDotnetRequirement(output);
158
+ const detail = requirement
159
+ ? dosaiDotnetRequirementDetail(requirement)
160
+ : "dosai could not start";
161
+ console.log(
162
+ `WARNING: ${detail}, so this deep run has no dosai evidence for .NET (call graph, reachability, crypto). ${dosaiDotnetRequirementAdvice(requirement)}`,
163
+ );
164
+ if (requirement) {
165
+ markDosaiLaunchFailureReported(dosaiBin);
166
+ }
167
+ recordLedgerEvent(LEDGER_EVENT_KINDS.EVIDENCE_DEGRADED, {
168
+ ecosystem: "csharp",
169
+ tool: "dosai",
170
+ detail: `${detail}; .NET evidence from dosai is unavailable.`,
171
+ impact: LEDGER_EVENT_IMPACTS.COMPONENTS,
172
+ });
173
+ deferFailOnError(options, {
174
+ ecosystem: "csharp",
175
+ tool: "dosai",
176
+ detail,
177
+ exitCode: typeof result?.status === "number" ? result.status : undefined,
178
+ command: `${dosaiBin} --version`,
179
+ outputExcerpt: output.trim() || undefined,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Whether the run covers .NET: a .NET project type, or, when cdxgen detects
185
+ * the types itself, a .NET project or solution file in the tree.
186
+ *
187
+ * @param {String} filePath Project path
188
+ * @param {Object} options CLI options
189
+ *
190
+ * @returns {Boolean}
191
+ */
192
+ function isDotnetInScope(filePath, options) {
193
+ if (options.projectType?.length) {
194
+ return hasAnyProjectType(["csharp"], options, false);
195
+ }
196
+ if (!hasAnyProjectType(["csharp"], options, true)) {
197
+ return false;
198
+ }
199
+ return (
200
+ getAllFiles(
201
+ filePath,
202
+ `${options.multiProject ? "**/" : ""}*.{csproj,vbproj,fsproj,sln,slnx}`,
203
+ options,
204
+ ).length > 0
205
+ );
206
+ }
207
+
107
208
  /**
108
209
  * Record the tool requirements the project declares through its well-known
109
210
  * tool pin files (`.tool-versions`, `.nvmrc`, `package.json`, `go.mod`, …),
@@ -1205,7 +1306,7 @@ export function prepareNodeEnv(filePath, options) {
1205
1306
  }
1206
1307
  }
1207
1308
  if (nvmNodePath) {
1208
- doNpmInstall(filePath, nvmNodePath);
1309
+ doNpmInstall(filePath, nvmNodePath, options);
1209
1310
  npmInstallAttempted = true;
1210
1311
  } else {
1211
1312
  console.log(
@@ -1228,7 +1329,7 @@ export function prepareNodeEnv(filePath, options) {
1228
1329
  );
1229
1330
  continue;
1230
1331
  }
1231
- doNpmInstall(filePath, nvmNodePath);
1332
+ doNpmInstall(filePath, nvmNodePath, options);
1232
1333
  npmInstallAttempted = true;
1233
1334
  }
1234
1335
  }
@@ -1302,12 +1403,18 @@ export function tryLoadNvmAndInstallTool(nodeVersion) {
1302
1403
  *
1303
1404
  * @param {String} filePath Path
1304
1405
  * @param {String} nvmNodePath Path to node version in nvm
1406
+ * @param {Object} [options] CLI options
1305
1407
  */
1306
- export function doNpmInstall(filePath, nvmNodePath) {
1408
+ export function doNpmInstall(filePath, nvmNodePath, options = {}) {
1307
1409
  // we do not install if INSTALL_ARGS set false
1308
1410
  if (["0", "false"].includes(readEnvironmentVariable("NODE_INSTALL_ARGS"))) {
1309
1411
  return;
1310
1412
  }
1413
+ // npm install runs the project's own lifecycle scripts, such as preinstall,
1414
+ // even with --package-lock-only.
1415
+ if (options.installDeps === false) {
1416
+ return;
1417
+ }
1311
1418
  const newPath = `${nvmNodePath}${delimiter}${readEnvironmentVariable("PATH")}`;
1312
1419
  let installArgs =
1313
1420
  readEnvironmentVariable("NPM_INSTALL_ARGS") || "--package-lock-only";
@@ -1386,7 +1493,12 @@ export function prepareSwiftEnv(filePath, options) {
1386
1493
  "INFO: Swift for Linux has known issues on non x64 machines. Run the cdxgen container image with --platform=linux/amd64 for best experience.",
1387
1494
  );
1388
1495
  }
1389
- if (options.deep || options?.lifecycle?.includes("post-build")) {
1496
+ // `swift package resolve` runs the project's Package.swift, so it waits for
1497
+ // the same permission as the scan's other project tooling.
1498
+ if (
1499
+ (options.deep || options?.lifecycle?.includes("post-build")) &&
1500
+ isProjectToolingAllowed(options)
1501
+ ) {
1390
1502
  const swiftFiles = getAllFiles(
1391
1503
  filePath,
1392
1504
  `${options.multiProject ? "**/" : ""}Package*.swift`,