@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
@@ -0,0 +1,218 @@
1
+ // Bounded Node.js build-version reader for unpackaged `node` binaries.
2
+ //
3
+ // The Node.js the Docker Official images, the release tarballs, nvm, fnm and
4
+ // volta install is not owned by any package manager, so a BOM sees it only as
5
+ // a file. Its version decides whether the runtime is affected by advisories
6
+ // such as CVE-2026-111497 (fixed in 22.23.4, 24.22.1 and 26.12.1).
7
+ //
8
+ // Two markers are compiled into the binary:
9
+ // - the release URLs, `https://nodejs.org/download/<channel>/v<version>/`,
10
+ // which a build configured with release URLs (every nodejs.org build)
11
+ // carries for process.release.sourceUrl and headersUrl
12
+ // https://github.com/nodejs/node/blob/v24.22.0/src/node_metadata.cc#L191-L197
13
+ // - `node.js/v<version>`, NUL-terminated, the inspector's /json/version
14
+ // Browser string, which every build with the inspector carries; it is the
15
+ // fallback for a build without release URLs.
16
+ // https://github.com/nodejs/node/blob/v24.22.0/src/inspector_socket_server.cc#L117
17
+ //
18
+ // process.versions.brotli is not a usable signal: Node.js backports the
19
+ // decoder fix without changing it (24.22.1 still reports 1.2.0). The decoder
20
+ // state is read separately, by brotliDecoder.js.
21
+
22
+ import { basename } from "node:path";
23
+
24
+ import { scanFileMarkers } from "../core/boundedFile.js";
25
+ import { applyPurl, tryBuildPurl } from "./purl.js";
26
+
27
+ const RELEASE_URL_MARKER = Buffer.from("https://nodejs.org/download/");
28
+ const USER_AGENT_MARKER = Buffer.from("node.js/v");
29
+ const MAX_VERSION_LENGTH = 64;
30
+ const VERSION_RE = /^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z][0-9A-Za-z.]*))?$/;
31
+ const CHANNELS = new Set(["release", "rc", "nightly", "test", "v8-canary"]);
32
+
33
+ /**
34
+ * The most node-named files one scan opens in search of a build version.
35
+ *
36
+ * @type {number}
37
+ */
38
+ export const MAX_NODE_PROBES = 64;
39
+
40
+ /**
41
+ * Whether a file name is one the Node.js runtime is installed under.
42
+ *
43
+ * @param {string} fileName basename or path of the candidate
44
+ * @returns {boolean} true for `node`, `nodejs` and `node.exe`
45
+ */
46
+ export function isNodeBinaryName(fileName) {
47
+ if (typeof fileName !== "string" || !fileName) {
48
+ return false;
49
+ }
50
+ const name = basename(fileName.replaceAll("\\", "/"));
51
+ return name === "node" || name === "nodejs" || name === "node.exe";
52
+ }
53
+
54
+ /**
55
+ * Parse a Node.js version in the grammar the release tooling produces:
56
+ * `24.22.1`, `25.0.0-rc.1`, `26.0.0-nightly20260301abcdef`.
57
+ *
58
+ * @param {string} text candidate version, without the leading `v`
59
+ * @returns {string|undefined} the version, or undefined
60
+ */
61
+ export function parseNodeVersion(text) {
62
+ if (typeof text !== "string" || !text || text.length > MAX_VERSION_LENGTH) {
63
+ return undefined;
64
+ }
65
+ return VERSION_RE.test(text) ? text : undefined;
66
+ }
67
+
68
+ function versionFromReleaseUrl(after) {
69
+ // <channel>/v<version>/...
70
+ const text = after.toString("latin1");
71
+ const slash = text.indexOf("/");
72
+ if (slash <= 0) {
73
+ return undefined;
74
+ }
75
+ const channel = text.slice(0, slash);
76
+ if (!CHANNELS.has(channel) || text[slash + 1] !== "v") {
77
+ return undefined;
78
+ }
79
+ const end = text.indexOf("/", slash + 2);
80
+ if (end === -1) {
81
+ return undefined;
82
+ }
83
+ const version = parseNodeVersion(text.slice(slash + 2, end));
84
+ return version ? { version, channel } : undefined;
85
+ }
86
+
87
+ function versionFromUserAgent(after) {
88
+ const end = after.indexOf(0);
89
+ if (end <= 0) {
90
+ return undefined;
91
+ }
92
+ return parseNodeVersion(after.toString("latin1", 0, end));
93
+ }
94
+
95
+ /**
96
+ * Read the Node.js version a `node` binary was built as.
97
+ *
98
+ * @param {string} absPath absolute path of the binary
99
+ * @param {string|undefined} rootDir extracted root, when the file came from one
100
+ * @returns {{version: string, source: string, channel?: string}|undefined}
101
+ * source is `release-url` or `binary`
102
+ */
103
+ export function readNodeBuildVersion(absPath, rootDir) {
104
+ const scan = scanFileMarkers(
105
+ absPath,
106
+ rootDir,
107
+ [RELEASE_URL_MARKER, USER_AGENT_MARKER],
108
+ { contextBytes: MAX_VERSION_LENGTH + 32, maxHits: 8 },
109
+ );
110
+ if (!scan || scan.skipped) {
111
+ return undefined;
112
+ }
113
+ const [releaseHits, userAgentHits] = scan.hits;
114
+ const fromBinary = userAgentHits
115
+ .map((hit) => versionFromUserAgent(hit.after))
116
+ .find(Boolean);
117
+ for (const hit of releaseHits) {
118
+ const release = versionFromReleaseUrl(hit.after);
119
+ // The headers URL names the release a binary was published as. A binary
120
+ // whose own version string disagrees is not that release, so the URL is
121
+ // used only when the two agree or the version string is missing.
122
+ if (release && (!fromBinary || fromBinary === release.version)) {
123
+ return {
124
+ version: release.version,
125
+ source: "release-url",
126
+ channel: release.channel,
127
+ };
128
+ }
129
+ }
130
+ return fromBinary ? { version: fromBinary, source: "binary" } : undefined;
131
+ }
132
+
133
+ /**
134
+ * The CPE that lets a downstream scanner match a Node.js version. NVD names the
135
+ * product node.js and keeps a pre-release in the update field.
136
+ *
137
+ * @param {string} version Node.js version
138
+ * @returns {string} CPE 2.3 string
139
+ */
140
+ export function nodeCpe(version) {
141
+ const match = VERSION_RE.exec(version || "");
142
+ if (!match) {
143
+ return `cpe:2.3:a:nodejs:node.js:${version}:*:*:*:*:*:*:*`;
144
+ }
145
+ const update = match[2] ? match[2].replaceAll(".", "").toLowerCase() : "*";
146
+ return `cpe:2.3:a:nodejs:node.js:${match[1]}:${update}:*:*:*:*:*:*`;
147
+ }
148
+
149
+ /**
150
+ * A Node.js runtime is one `platform` component per distinct version, like the
151
+ * PHP and Go stdlib components. Its identity is `pkg:generic/node`: it is only
152
+ * used where no package manager owns the runtime.
153
+ *
154
+ * @param {string} version Node.js version
155
+ * @returns {Object} component with `purl`, `bom-ref` and `cpe`
156
+ */
157
+ export function createNodeRuntimeComponent(version) {
158
+ return applyPurl(
159
+ { type: "platform", name: "node", version, cpe: nodeCpe(version) },
160
+ tryBuildPurl({ type: "generic", name: "node", version }),
161
+ );
162
+ }
163
+
164
+ /**
165
+ * The properties a node file component carries once its build is known.
166
+ *
167
+ * @param {{version: string, source: string, channel?: string}} build result of readNodeBuildVersion
168
+ * @returns {{name: string, value: string}[]} properties to append
169
+ */
170
+ export function nodeBuildProperties(build) {
171
+ const properties = [
172
+ { name: "cdx:node:buildVersion", value: build.version },
173
+ { name: "cdx:node:buildVersionSource", value: build.source },
174
+ ];
175
+ if (build.channel) {
176
+ properties.push({ name: "cdx:node:releaseChannel", value: build.channel });
177
+ }
178
+ return properties;
179
+ }
180
+
181
+ /**
182
+ * Start collecting the runtime components and edges of one scan.
183
+ *
184
+ * @returns {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}}
185
+ */
186
+ export function createNodeRuntimeInventory() {
187
+ return { runtimeComponents: [], dependencies: [], runtimeRefs: new Set() };
188
+ }
189
+
190
+ /**
191
+ * Tag a node file component with its build facts and link it to the runtime
192
+ * component of its version, creating that component on first use. Mutates the
193
+ * component in place.
194
+ *
195
+ * @param {{runtimeComponents: Object[], dependencies: Object[], runtimeRefs: Set<string>}} inventory
196
+ * accumulator from createNodeRuntimeInventory
197
+ * @param {Object} component file component, with a `properties` array
198
+ * @param {{version: string, source: string, channel?: string}} build result of readNodeBuildVersion
199
+ */
200
+ export function attachNodeBuild(inventory, component, build) {
201
+ component.properties.push(...nodeBuildProperties(build));
202
+ const runtime = createNodeRuntimeComponent(build.version);
203
+ const runtimeRef = runtime["bom-ref"];
204
+ if (!runtimeRef) {
205
+ return;
206
+ }
207
+ if (!inventory.runtimeRefs.has(runtimeRef)) {
208
+ inventory.runtimeRefs.add(runtimeRef);
209
+ inventory.runtimeComponents.push(runtime);
210
+ inventory.dependencies.push({ ref: runtimeRef, dependsOn: [] });
211
+ }
212
+ if (component["bom-ref"]) {
213
+ inventory.dependencies.push({
214
+ ref: component["bom-ref"],
215
+ dependsOn: [runtimeRef],
216
+ });
217
+ }
218
+ }
@@ -0,0 +1,202 @@
1
+ // Bounded, read-only probe for the Node.js no package manager owns on a live
2
+ // host, for `-t os` / obom. The release tarballs, the nodejs.org installer and
3
+ // the version managers (nvm, fnm, volta, asdf, mise, n) put Node.js where no
4
+ // package database records it, so an OBOM sees neither the runtime nor the
5
+ // Brotli decoder compiled into it.
6
+ //
7
+ // Candidates come from fixed directories and from each version manager's
8
+ // tree under the home directories, one wildcard level at a time, never from a
9
+ // walk of the file system. The ownership rules, caps and degradations are the
10
+ // shared host-runtime probe's.
11
+
12
+ import { join } from "node:path";
13
+
14
+ import {
15
+ isDryRun,
16
+ readEnvironmentVariable,
17
+ recordActivity,
18
+ } from "../core/activity.js";
19
+ import { recordDegradation } from "../core/buildLedger.js";
20
+ import {
21
+ attachBrotliDecoder,
22
+ createBrotliInventory,
23
+ finishBrotliInventory,
24
+ readBrotliDecoder,
25
+ } from "./brotliDecoder.js";
26
+ import {
27
+ isDirectory,
28
+ listDirectory,
29
+ probeHostRuntime,
30
+ } from "./hostRuntimeProbe.js";
31
+ import {
32
+ attachNodeBuild,
33
+ createNodeRuntimeInventory,
34
+ isNodeBinaryName,
35
+ MAX_NODE_PROBES,
36
+ readNodeBuildVersion,
37
+ } from "./nodeBuildInfo.js";
38
+
39
+ // The most directories one wildcard pattern may expand to.
40
+ const MAX_EXPANDED_DIRECTORIES = 256;
41
+
42
+ // Relative to the root.
43
+ const POSIX_DIRECTORIES = {
44
+ linux: ["usr/local/bin", "opt/*/bin", "usr/local/n/versions/node/*/bin"],
45
+ darwin: ["usr/local/bin", "opt/homebrew/bin", "opt/*/bin"],
46
+ };
47
+
48
+ // Relative to a home directory.
49
+ const VERSION_MANAGER_DIRECTORIES = [
50
+ ".nvm/versions/node/*/bin",
51
+ ".local/share/fnm/node-versions/*/installation/bin",
52
+ ".fnm/node-versions/*/installation/bin",
53
+ "Library/Application Support/fnm/node-versions/*/installation/bin",
54
+ ".volta/tools/image/node/*/bin",
55
+ ".asdf/installs/nodejs/*/bin",
56
+ ".local/share/mise/installs/node/*/bin",
57
+ ];
58
+
59
+ const HOME_PARENTS = {
60
+ linux: ["home"],
61
+ darwin: ["Users"],
62
+ };
63
+
64
+ /**
65
+ * Expand a relative directory pattern whose `*` segments match one directory
66
+ * name each.
67
+ *
68
+ * @param {string} base absolute directory the pattern hangs off
69
+ * @param {string} pattern `/`-separated relative pattern
70
+ * @returns {string[]} existing directories, at most MAX_EXPANDED_DIRECTORIES
71
+ */
72
+ function expandDirectories(base, pattern) {
73
+ let current = [base];
74
+ for (const segment of pattern.split("/")) {
75
+ const next = [];
76
+ for (const directory of current) {
77
+ if (segment !== "*") {
78
+ next.push(join(directory, segment));
79
+ continue;
80
+ }
81
+ for (const name of listDirectory(directory)) {
82
+ const child = join(directory, name);
83
+ if (isDirectory(child)) {
84
+ next.push(child);
85
+ }
86
+ if (next.length >= MAX_EXPANDED_DIRECTORIES) {
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ current = next.slice(0, MAX_EXPANDED_DIRECTORIES);
92
+ }
93
+ return current.filter(isDirectory);
94
+ }
95
+
96
+ function homeDirectories(context) {
97
+ const { rootPath, platform } = context;
98
+ const homes = [];
99
+ if (platform === "linux") {
100
+ homes.push(join(rootPath, "root"));
101
+ }
102
+ for (const parent of HOME_PARENTS[platform] || []) {
103
+ for (const name of listDirectory(join(rootPath, parent))) {
104
+ const home = join(rootPath, parent, name);
105
+ if (isDirectory(home)) {
106
+ homes.push(home);
107
+ }
108
+ }
109
+ }
110
+ return homes.slice(0, MAX_EXPANDED_DIRECTORIES);
111
+ }
112
+
113
+ // Where the Node.js MSI installs, by itself or for Chocolatey's nodejs.install.
114
+ function windowsInstallDirectory(context) {
115
+ const programFiles =
116
+ context.readEnv("ProgramFiles") || join(context.rootPath, "Program Files");
117
+ return join(programFiles, "nodejs");
118
+ }
119
+
120
+ function candidateDirectories(context) {
121
+ const { rootPath, platform } = context;
122
+ const directories = [];
123
+ if (platform === "win32") {
124
+ directories.push({ directory: windowsInstallDirectory(context) });
125
+ const appData = context.readEnv("APPDATA");
126
+ if (appData) {
127
+ for (const directory of expandDirectories(appData, "nvm/*")) {
128
+ directories.push({ directory });
129
+ }
130
+ }
131
+ return directories;
132
+ }
133
+ for (const pattern of POSIX_DIRECTORIES[platform] || []) {
134
+ for (const directory of expandDirectories(rootPath, pattern)) {
135
+ directories.push({ directory });
136
+ }
137
+ }
138
+ for (const home of homeDirectories(context)) {
139
+ for (const pattern of VERSION_MANAGER_DIRECTORIES) {
140
+ for (const directory of expandDirectories(home, pattern)) {
141
+ directories.push({ directory });
142
+ }
143
+ }
144
+ }
145
+ return directories;
146
+ }
147
+
148
+ function readBuild(context, candidate) {
149
+ const node = readNodeBuildVersion(candidate.real, context.realRoot);
150
+ if (!node) {
151
+ return undefined;
152
+ }
153
+ return { node, brotli: readBrotliDecoder(candidate.real, context.realRoot) };
154
+ }
155
+
156
+ /**
157
+ * Look for Node.js no package manager owns on the host. Components come back
158
+ * only for the files nothing owns: a `file` component for each `node` binary
159
+ * with the cdx:node and cdx:brotli properties, one `platform` component per
160
+ * Node.js version with its CPE, and the brotli component when a binary embeds
161
+ * a decoder.
162
+ *
163
+ * @param {Object} [options] probe options, as probeHostPhp takes them
164
+ * @returns {{components: Object[], dependencies: Object[]}}
165
+ */
166
+ export function probeHostNode(options = {}) {
167
+ return probeHostRuntime(options, {
168
+ label: "Node.js",
169
+ ecosystem: "npm",
170
+ capability: "node-host-probe",
171
+ remediationId: "npm.host-probe.package-db-unreadable",
172
+ maxProbes: MAX_NODE_PROBES,
173
+ chocolateyPackages: ["nodejs", "nodejs-lts", "nodejs.install"],
174
+ chocolateyDirectories: (context) => [windowsInstallDirectory(context)],
175
+ directories: candidateDirectories,
176
+ classify: (name) => (isNodeBinaryName(name) ? "node" : undefined),
177
+ readBuild,
178
+ createInventory: () => ({
179
+ node: createNodeRuntimeInventory(),
180
+ brotli: createBrotliInventory(),
181
+ }),
182
+ attach: (inventory, component, build) => {
183
+ attachNodeBuild(inventory.node, component, build.node);
184
+ if (build.brotli) {
185
+ attachBrotliDecoder(inventory.brotli, component, build.brotli);
186
+ }
187
+ },
188
+ finish: (inventory) => {
189
+ const brotli = finishBrotliInventory(inventory.brotli);
190
+ return {
191
+ components: [...inventory.node.runtimeComponents, ...brotli.components],
192
+ dependencies: [...inventory.node.dependencies, ...brotli.dependencies],
193
+ };
194
+ },
195
+ hooks: {
196
+ isDryRun,
197
+ readEnvironmentVariable,
198
+ recordActivity,
199
+ recordDegradation,
200
+ },
201
+ });
202
+ }
@@ -0,0 +1,244 @@
1
+ // The one place that decides what purl an operating-system package gets.
2
+ //
3
+ // A package found by `cdxgen -t docker <image>` and by `obom` run inside that
4
+ // image must be the same package, so the OBOM rows, the file-to-package
5
+ // resolver and the container scan all name a deb, an rpm or an apk package with
6
+ // the vendor and the qualifiers set here and in getDistroInfo(): `arch`,
7
+ // `distro`, `distro_name` and, when the package has one, `epoch`. A pacman
8
+ // package has the vendor and only the `arch` qualifier the alpm type defines.
9
+ // Homebrew, MacPorts and snap packages have no distro, so their purl is the
10
+ // name and the version; a flatpak adds the `arch` and `branch` of its ref.
11
+ //
12
+ // None of these purls has a subpath. A subpath names a path inside a package;
13
+ // the dpkg directory or the source rpm of a row is not one.
14
+
15
+ import { getDistroInfo, isCleanDistroQualifier } from "./osinfo.js";
16
+ import { tryBuildPurl } from "./purl.js";
17
+
18
+ /**
19
+ * The purl types an OS scan builds from what a package manager reports.
20
+ * deb, rpm, apk and alpm belong to a distro; brew, macports, snap and flatpak
21
+ * belong to none. brew, macports, snap and flatpak are not purl-spec types;
22
+ * cdx-purl builds them as it builds `pkg:chocolatey`.
23
+ */
24
+ export const OS_PACKAGE_PURL_TYPES = new Set([
25
+ "deb",
26
+ "rpm",
27
+ "apk",
28
+ "alpm",
29
+ "brew",
30
+ "macports",
31
+ "snap",
32
+ "flatpak",
33
+ ]);
34
+
35
+ /**
36
+ * Whether a purl type is one of the OS package managers handled here.
37
+ *
38
+ * @param {string|undefined} type Purl type
39
+ * @returns {boolean} true for the types in OS_PACKAGE_PURL_TYPES
40
+ */
41
+ export function isOsPackagePurlType(type) {
42
+ return OS_PACKAGE_PURL_TYPES.has(type);
43
+ }
44
+
45
+ /**
46
+ * Whether a value names an architecture. rpm says `(none)` and trivy says
47
+ * `None` for a package that has none, such as an rpm's `gpg-pubkey` key record;
48
+ * neither is an architecture, and neither belongs in a purl.
49
+ *
50
+ * @param {string|undefined|null} arch Architecture as a package manager reports it
51
+ * @returns {boolean} true for a real architecture
52
+ */
53
+ export function isRealArchitecture(arch) {
54
+ const value = `${arch ?? ""}`.trim().toLowerCase();
55
+ return value !== "" && value !== "(none)" && value !== "none";
56
+ }
57
+
58
+ function isDigits(text) {
59
+ if (!text) {
60
+ return false;
61
+ }
62
+ for (let i = 0; i < text.length; i++) {
63
+ const code = text.charCodeAt(i);
64
+ if (code < 48 || code > 57) {
65
+ return false;
66
+ }
67
+ }
68
+ return true;
69
+ }
70
+
71
+ // An epoch is a non-negative integer, and 0 is the same as none. Anything else
72
+ // ("", "(none)", "null") is no epoch.
73
+ function normaliseEpoch(value) {
74
+ const text = `${value ?? ""}`.trim();
75
+ if (!isDigits(text)) {
76
+ return undefined;
77
+ }
78
+ const digits = text.replace(/^0+/, "");
79
+ return digits || undefined;
80
+ }
81
+
82
+ // dpkg writes a version as [epoch:]upstream[-revision]. A colon is only ever an
83
+ // epoch's separator when what comes before it is all digits.
84
+ function debVersionEpoch(version) {
85
+ const colon = version.indexOf(":");
86
+ if (colon <= 0) {
87
+ return undefined;
88
+ }
89
+ const prefix = version.slice(0, colon);
90
+ return isDigits(prefix) ? prefix : undefined;
91
+ }
92
+
93
+ /**
94
+ * The epoch of an OS package, as the `epoch` qualifier carries it.
95
+ *
96
+ * An rpm keeps its epoch in a field of its own. A deb keeps it in its version
97
+ * (`2:93`), so the version is read first. Epoch 0 and no epoch are the same,
98
+ * and apk and Homebrew packages have none.
99
+ *
100
+ * @param {{type: string, version?: string, epoch?: string|number}} pkg Package
101
+ * @returns {string|undefined} The epoch without leading zeros, or undefined
102
+ */
103
+ export function osPackageEpoch(pkg) {
104
+ switch (pkg?.type) {
105
+ case "rpm":
106
+ return normaliseEpoch(pkg.epoch);
107
+ case "deb": {
108
+ const fromVersion = debVersionEpoch(`${pkg.version ?? ""}`);
109
+ return fromVersion === undefined
110
+ ? normaliseEpoch(pkg.epoch)
111
+ : normaliseEpoch(fromVersion);
112
+ }
113
+ default:
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * The version an OS package carries on its component and in its purl.
120
+ *
121
+ * - rpm: `[epoch:]version-release`, the form the container scan writes. The
122
+ * purl spec keeps the epoch out of an rpm version, but the container scan
123
+ * puts it in the version as well as in the `epoch` qualifier, and an OBOM must
124
+ * name the package the same way.
125
+ * - deb: the version exactly as dpkg reports it, epoch included.
126
+ * - macports: `version_revision`, the form `port installed` prints.
127
+ * - apk, alpm, brew, snap and flatpak: the version as it is. A pacman version
128
+ * already holds its epoch and release (`1:1.3.2-4`).
129
+ *
130
+ * @param {{type: string, version?: string, release?: string, epoch?: string|number, revision?: string|number}} pkg Package
131
+ * @returns {string} The version, or an empty string when the package has none
132
+ */
133
+ export function osPackageVersion(pkg) {
134
+ const version = `${pkg?.version ?? ""}`;
135
+ if (!version) {
136
+ return "";
137
+ }
138
+ switch (pkg.type) {
139
+ case "rpm": {
140
+ const release = `${pkg.release ?? ""}`;
141
+ const evr = release ? `${version}-${release}` : version;
142
+ const epoch = osPackageEpoch(pkg);
143
+ return epoch ? `${epoch}:${evr}` : evr;
144
+ }
145
+ case "deb": {
146
+ if (debVersionEpoch(version) !== undefined) {
147
+ return version;
148
+ }
149
+ const epoch = normaliseEpoch(pkg.epoch);
150
+ return epoch ? `${epoch}:${version}` : version;
151
+ }
152
+ case "macports": {
153
+ // Revision 0 is a real revision: `port installed` prints `@3.6.0_0`.
154
+ const revision = `${pkg.revision ?? ""}`.trim();
155
+ return isDigits(revision) ? `${version}_${revision}` : version;
156
+ }
157
+ default:
158
+ return version;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Build the purl of an OS package.
164
+ *
165
+ * deb, rpm and apk purls take their vendor (`ubuntu`, `almalinux`, `alpine`,
166
+ * `wolfi`) and the `distro` and `distro_name` qualifiers from the distro, plus
167
+ * `arch` and `epoch` from the package. alpm purls take the vendor (`arch`,
168
+ * `manjaro`) and `arch` only: that is all the alpm type defines, and an Arch
169
+ * system is rolling, with no release to name. A qualifier with no value is
170
+ * left out. brew, macports and snap purls carry neither vendor nor qualifiers;
171
+ * a flatpak purl carries the `arch` and `branch` of the flatpak ref.
172
+ *
173
+ * @param {Object} pkg The package
174
+ * @param {string} pkg.type One of OS_PACKAGE_PURL_TYPES
175
+ * @param {string} pkg.name Package name; a flatpak's application ID
176
+ * @param {string} [pkg.version] deb: the dpkg version. rpm: the version tag only. apk: version-rN. macports: the version without its revision.
177
+ * @param {string} [pkg.release] rpm release tag
178
+ * @param {string|number} [pkg.epoch] rpm epoch tag
179
+ * @param {string|number} [pkg.revision] MacPorts revision
180
+ * @param {string} [pkg.arch] Package architecture
181
+ * @param {string} [pkg.branch] flatpak branch (`stable`, `48`)
182
+ * @param {{namespace?: string, distroId?: string, distroName?: string}} [distro]
183
+ * What getDistroInfo() reports; read from the host's os-release when left out
184
+ * @returns {string|null} The purl, or null when the parts do not form a valid one
185
+ */
186
+ export function buildOsPackagePurl(pkg, distro) {
187
+ const type = pkg?.type;
188
+ const name = `${pkg?.name ?? ""}`;
189
+ if (!isOsPackagePurlType(type) || !name) {
190
+ return null;
191
+ }
192
+ const version = osPackageVersion(pkg) || null;
193
+ switch (type) {
194
+ case "brew":
195
+ case "macports":
196
+ case "snap":
197
+ return tryBuildPurl({ type, name, version });
198
+ case "flatpak": {
199
+ const qualifiers = {};
200
+ if (isRealArchitecture(pkg.arch)) {
201
+ qualifiers.arch = `${pkg.arch}`.trim();
202
+ }
203
+ const branch = `${pkg.branch ?? ""}`.trim();
204
+ if (branch) {
205
+ qualifiers.branch = branch;
206
+ }
207
+ return tryBuildPurl({
208
+ type,
209
+ name,
210
+ version,
211
+ qualifiers: Object.keys(qualifiers).length ? qualifiers : null,
212
+ });
213
+ }
214
+ default:
215
+ break;
216
+ }
217
+ const { namespace, distroId, distroName } = distro || getDistroInfo();
218
+ if (!namespace) {
219
+ return null;
220
+ }
221
+ const qualifiers = {};
222
+ if (isRealArchitecture(pkg.arch)) {
223
+ qualifiers.arch = `${pkg.arch}`.trim();
224
+ }
225
+ if (type !== "alpm") {
226
+ if (isCleanDistroQualifier(distroId)) {
227
+ qualifiers.distro = distroId;
228
+ }
229
+ if (isCleanDistroQualifier(distroName)) {
230
+ qualifiers.distro_name = distroName;
231
+ }
232
+ const epoch = osPackageEpoch(pkg);
233
+ if (epoch) {
234
+ qualifiers.epoch = epoch;
235
+ }
236
+ }
237
+ return tryBuildPurl({
238
+ type,
239
+ namespace,
240
+ name,
241
+ version,
242
+ qualifiers: Object.keys(qualifiers).length ? qualifiers : null,
243
+ });
244
+ }