@cyclonedx/cdxgen 12.3.3 → 12.4.1

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 (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -0,0 +1,350 @@
1
+ import { arch as runtimeArch, platform as runtimePlatform } from "node:os";
2
+ import { delimiter, join } from "node:path";
3
+ import process from "node:process";
4
+
5
+ import {
6
+ DEBUG_MODE,
7
+ dirNameStr,
8
+ retrieveCdxgenPluginVersion,
9
+ safeExistsSync,
10
+ safeSpawnSync,
11
+ } from "./utils.js";
12
+
13
+ const PLUGIN_ENV_COMMAND_NAMES = {
14
+ "cargo-auditable": "CARGO_AUDITABLE_CMD",
15
+ dosai: "DOSAI_CMD",
16
+ osquery: "OSQUERY_CMD",
17
+ sourcekitten: "SOURCEKITTEN_CMD",
18
+ trivy: "TRIVY_CMD",
19
+ trustinspector: "TRUSTINSPECTOR_CMD",
20
+ };
21
+
22
+ function isMusl() {
23
+ const result = safeSpawnSync("ldd", ["--version"]);
24
+ return result?.stdout?.includes("musl") || result?.stderr?.includes("musl");
25
+ }
26
+
27
+ function hasUsablePluginsDir(pluginsDir) {
28
+ return (
29
+ safeExistsSync(pluginsDir) &&
30
+ (safeExistsSync(join(pluginsDir, "plugins-manifest.json")) ||
31
+ [
32
+ "cargo-auditable",
33
+ "dosai",
34
+ "osquery",
35
+ "sourcekitten",
36
+ "trivy",
37
+ "trustinspector",
38
+ ].some((pluginName) => safeExistsSync(join(pluginsDir, pluginName))))
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Determine the normalized plugin target tuple for the current runtime.
44
+ *
45
+ * @returns {{arch: string, extn: string, platform: string, pluginsBinSuffix: string}}
46
+ */
47
+ export function getPluginsBinTarget() {
48
+ let platform = runtimePlatform();
49
+ let extn = "";
50
+ let pluginsBinSuffix = "";
51
+ if (platform === "win32") {
52
+ platform = "windows";
53
+ extn = ".exe";
54
+ } else if (platform === "linux" && isMusl()) {
55
+ platform = "linuxmusl";
56
+ }
57
+
58
+ let arch = `${runtimeArch()}`;
59
+ if (arch === "x32") {
60
+ arch = "386";
61
+ } else if (arch === "x64") {
62
+ arch = "amd64";
63
+ pluginsBinSuffix = `-${platform}-amd64`;
64
+ } else if (arch === "arm64") {
65
+ pluginsBinSuffix = `-${platform}-arm64`;
66
+ } else if (arch === "ppc64") {
67
+ arch = "ppc64le";
68
+ pluginsBinSuffix = "-ppc64";
69
+ }
70
+
71
+ return {
72
+ arch,
73
+ extn,
74
+ platform,
75
+ pluginsBinSuffix,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Resolve the cdxgen companion plugins directory for the current runtime.
81
+ *
82
+ * @returns {{
83
+ * arch: string,
84
+ * extn: string,
85
+ * extraNMBinPath: string|undefined,
86
+ * platform: string,
87
+ * pluginManifestFile: string|undefined,
88
+ * pluginVersion: string|undefined,
89
+ * pluginsBinSuffix: string,
90
+ * pluginsDir: string,
91
+ * }}
92
+ */
93
+ export function resolveCdxgenPlugins() {
94
+ const target = getPluginsBinTarget();
95
+ const pluginVersion = retrieveCdxgenPluginVersion();
96
+ let pluginsDir = process.env.CDXGEN_PLUGINS_DIR || "";
97
+ let extraNMBinPath;
98
+
99
+ if (!pluginsDir && hasUsablePluginsDir(join(dirNameStr, "plugins"))) {
100
+ pluginsDir = join(dirNameStr, "plugins");
101
+ }
102
+
103
+ if (
104
+ !pluginsDir &&
105
+ hasUsablePluginsDir(
106
+ join(
107
+ dirNameStr,
108
+ "node_modules",
109
+ "@cdxgen",
110
+ `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
111
+ "plugins",
112
+ ),
113
+ )
114
+ ) {
115
+ pluginsDir = join(
116
+ dirNameStr,
117
+ "node_modules",
118
+ "@cdxgen",
119
+ `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
120
+ "plugins",
121
+ );
122
+ if (safeExistsSync(join(dirNameStr, "node_modules", ".bin"))) {
123
+ extraNMBinPath = join(dirNameStr, "node_modules", ".bin");
124
+ }
125
+ }
126
+
127
+ if (!pluginsDir) {
128
+ let globalNodePath = process.env.GLOBAL_NODE_MODULES_PATH || undefined;
129
+ if (!globalNodePath) {
130
+ if (DEBUG_MODE) {
131
+ console.log(
132
+ 'Trying to find the global node_modules path with "pnpm root -g" command.',
133
+ );
134
+ }
135
+ const result = safeSpawnSync(
136
+ target.platform === "windows" ? "pnpm.cmd" : "pnpm",
137
+ ["root", "-g"],
138
+ );
139
+ if (result?.stdout) {
140
+ globalNodePath = `${result.stdout.trim()}/`;
141
+ }
142
+ }
143
+
144
+ let globalPlugins;
145
+ if (globalNodePath) {
146
+ globalPlugins = join(
147
+ globalNodePath,
148
+ "@cdxgen",
149
+ `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
150
+ "plugins",
151
+ );
152
+ extraNMBinPath = join(
153
+ globalNodePath,
154
+ "..",
155
+ ".pnpm",
156
+ "node_modules",
157
+ ".bin",
158
+ );
159
+ }
160
+
161
+ let altGlobalPlugins;
162
+ if (
163
+ dirNameStr.includes(join("node_modules", ".pnpm", "@cyclonedx+cdxgen"))
164
+ ) {
165
+ const tmpA = dirNameStr.split(join("node_modules", ".pnpm"));
166
+ altGlobalPlugins = join(
167
+ tmpA[0],
168
+ "node_modules",
169
+ ".pnpm",
170
+ `@cdxgen+cdxgen-plugins-bin${target.pluginsBinSuffix}@${pluginVersion}`,
171
+ "node_modules",
172
+ "@cdxgen",
173
+ `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
174
+ "plugins",
175
+ );
176
+ if (safeExistsSync(join(tmpA[0], "node_modules", ".bin"))) {
177
+ extraNMBinPath = join(tmpA[0], "node_modules", ".bin");
178
+ }
179
+ } else if (dirNameStr.includes(join(".pnpm", "@cyclonedx+cdxgen"))) {
180
+ const tmpA = dirNameStr.split(".pnpm");
181
+ altGlobalPlugins = join(
182
+ tmpA[0],
183
+ ".pnpm",
184
+ `@cdxgen+cdxgen-plugins-bin${target.pluginsBinSuffix}@${pluginVersion}`,
185
+ "node_modules",
186
+ "@cdxgen",
187
+ `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
188
+ "plugins",
189
+ );
190
+ if (safeExistsSync(join(tmpA[0], ".bin"))) {
191
+ extraNMBinPath = join(tmpA[0], ".bin");
192
+ }
193
+ } else if (dirNameStr.includes(join("caxa", "applications"))) {
194
+ altGlobalPlugins = join(
195
+ dirNameStr,
196
+ "node_modules",
197
+ "pnpm",
198
+ `@cdxgen+cdxgen-plugins-bin${target.pluginsBinSuffix}@${pluginVersion}`,
199
+ "node_modules",
200
+ "@cdxgen",
201
+ `cdxgen-plugins-bin${target.pluginsBinSuffix}`,
202
+ "plugins",
203
+ );
204
+ extraNMBinPath = join(dirNameStr, "node_modules", ".bin");
205
+ }
206
+
207
+ if (globalPlugins && safeExistsSync(globalPlugins)) {
208
+ pluginsDir = globalPlugins;
209
+ if (DEBUG_MODE) {
210
+ console.log("Found global plugins", pluginsDir);
211
+ }
212
+ } else if (altGlobalPlugins && safeExistsSync(altGlobalPlugins)) {
213
+ pluginsDir = altGlobalPlugins;
214
+ if (DEBUG_MODE) {
215
+ console.log("Found global plugins", pluginsDir);
216
+ }
217
+ }
218
+ }
219
+
220
+ if (!pluginsDir) {
221
+ if (DEBUG_MODE) {
222
+ console.warn(
223
+ "The optional cdxgen plugin was not found. Please install cdxgen without excluding optional dependencies if needed.",
224
+ );
225
+ }
226
+ pluginsDir = "";
227
+ }
228
+
229
+ const pluginManifestFile = safeExistsSync(
230
+ join(pluginsDir, "plugins-manifest.json"),
231
+ )
232
+ ? join(pluginsDir, "plugins-manifest.json")
233
+ : undefined;
234
+
235
+ return {
236
+ ...target,
237
+ extraNMBinPath,
238
+ pluginManifestFile,
239
+ pluginVersion,
240
+ pluginsDir,
241
+ };
242
+ }
243
+
244
+ function getPluginRuntimeCacheKey() {
245
+ return [
246
+ process.env.CDXGEN_PLUGINS_DIR || "",
247
+ process.env.GLOBAL_NODE_MODULES_PATH || "",
248
+ ].join("\u0000");
249
+ }
250
+
251
+ let cachedPluginRuntime;
252
+ let cachedPluginRuntimeKey;
253
+
254
+ /**
255
+ * Retrieve the default plugin runtime, recomputing it only when the
256
+ * environment that influences plugin discovery changes.
257
+ *
258
+ * @returns {ReturnType<typeof resolveCdxgenPlugins>} The resolved plugin runtime.
259
+ */
260
+ export function getDefaultPluginRuntime() {
261
+ const cacheKey = getPluginRuntimeCacheKey();
262
+ if (!cachedPluginRuntime || cachedPluginRuntimeKey !== cacheKey) {
263
+ cachedPluginRuntime = resolveCdxgenPlugins();
264
+ cachedPluginRuntimeKey = cacheKey;
265
+ }
266
+ return cachedPluginRuntime;
267
+ }
268
+
269
+ /**
270
+ * Add the detected node_modules binary directory to PATH when present.
271
+ *
272
+ * @param {ReturnType<typeof resolveCdxgenPlugins>} [pluginRuntime] Detected plugin runtime.
273
+ * @returns {ReturnType<typeof resolveCdxgenPlugins>} The resolved plugin runtime.
274
+ */
275
+ export function setPluginsPathEnv(pluginRuntime = undefined) {
276
+ pluginRuntime ??= getDefaultPluginRuntime();
277
+ if (
278
+ pluginRuntime.extraNMBinPath &&
279
+ !process.env?.PATH?.includes(pluginRuntime.extraNMBinPath)
280
+ ) {
281
+ process.env.PATH = `${pluginRuntime.extraNMBinPath}${delimiter}${process.env.PATH}`;
282
+ }
283
+ return pluginRuntime;
284
+ }
285
+
286
+ function resolveBundledPluginBinary(toolName, pluginRuntime) {
287
+ if (!pluginRuntime.pluginsDir) {
288
+ return undefined;
289
+ }
290
+ if (!safeExistsSync(join(pluginRuntime.pluginsDir, toolName))) {
291
+ return undefined;
292
+ }
293
+ switch (toolName) {
294
+ case "trivy":
295
+ return join(
296
+ pluginRuntime.pluginsDir,
297
+ "trivy",
298
+ `trivy-cdxgen-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
299
+ );
300
+ case "cargo-auditable":
301
+ return join(
302
+ pluginRuntime.pluginsDir,
303
+ "cargo-auditable",
304
+ `cargo-auditable-cdxgen-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
305
+ );
306
+ case "osquery": {
307
+ let osqueryBin = join(
308
+ pluginRuntime.pluginsDir,
309
+ "osquery",
310
+ `osqueryi-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
311
+ );
312
+ if (pluginRuntime.platform === "darwin") {
313
+ osqueryBin = `${osqueryBin}.app/Contents/MacOS/osqueryd`;
314
+ }
315
+ return osqueryBin;
316
+ }
317
+ case "dosai":
318
+ return join(
319
+ pluginRuntime.pluginsDir,
320
+ "dosai",
321
+ `dosai-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
322
+ );
323
+ case "trustinspector":
324
+ return join(
325
+ pluginRuntime.pluginsDir,
326
+ "trustinspector",
327
+ `trustinspector-cdxgen-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
328
+ );
329
+ case "sourcekitten":
330
+ return join(pluginRuntime.pluginsDir, "sourcekitten", "sourcekitten");
331
+ default:
332
+ return undefined;
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Resolve a known plugin binary path, honoring explicit environment overrides.
338
+ *
339
+ * @param {string} toolName Tool identifier.
340
+ * @param {ReturnType<typeof resolveCdxgenPlugins>} [pluginRuntime] Detected plugin runtime.
341
+ * @returns {string|undefined} Resolved binary path or configured override.
342
+ */
343
+ export function resolvePluginBinary(toolName, pluginRuntime = undefined) {
344
+ pluginRuntime ??= getDefaultPluginRuntime();
345
+ const envCommandName = PLUGIN_ENV_COMMAND_NAMES[toolName];
346
+ if (envCommandName && process.env[envCommandName]) {
347
+ return process.env[envCommandName];
348
+ }
349
+ return resolveBundledPluginBinary(toolName, pluginRuntime);
350
+ }
@@ -0,0 +1,57 @@
1
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import process from "node:process";
5
+
6
+ import { assert, describe, it } from "poku";
7
+
8
+ import { resolveCdxgenPlugins, resolvePluginBinary } from "./plugins.js";
9
+
10
+ describe("plugins helper", () => {
11
+ it("resolvePluginBinary() prefers explicit OSQUERY_CMD overrides", () => {
12
+ const previousOsqueryCmd = process.env.OSQUERY_CMD;
13
+ try {
14
+ process.env.OSQUERY_CMD = "/tmp/osqueryd";
15
+ assert.strictEqual(resolvePluginBinary("osquery"), "/tmp/osqueryd");
16
+ } finally {
17
+ if (previousOsqueryCmd === undefined) {
18
+ delete process.env.OSQUERY_CMD;
19
+ } else {
20
+ process.env.OSQUERY_CMD = previousOsqueryCmd;
21
+ }
22
+ }
23
+ });
24
+
25
+ it("resolveCdxgenPlugins() honors CDXGEN_PLUGINS_DIR for bundled osquery binaries", () => {
26
+ const pluginsDir = mkdtempSync(join(tmpdir(), "cdxgen-plugins-helper-"));
27
+ const previousPluginsDir = process.env.CDXGEN_PLUGINS_DIR;
28
+ try {
29
+ mkdirSync(join(pluginsDir, "osquery"), { recursive: true });
30
+ process.env.CDXGEN_PLUGINS_DIR = pluginsDir;
31
+ const pluginRuntime = resolveCdxgenPlugins();
32
+ const osqueryBinary = resolvePluginBinary("osquery", pluginRuntime);
33
+ const expectedPrefix = join(
34
+ pluginsDir,
35
+ "osquery",
36
+ `osqueryi-${pluginRuntime.platform}-${pluginRuntime.arch}${pluginRuntime.extn}`,
37
+ );
38
+
39
+ assert.strictEqual(pluginRuntime.pluginsDir, pluginsDir);
40
+ if (pluginRuntime.platform === "darwin") {
41
+ assert.strictEqual(
42
+ osqueryBinary,
43
+ `${expectedPrefix}.app/Contents/MacOS/osqueryd`,
44
+ );
45
+ } else {
46
+ assert.strictEqual(osqueryBinary, expectedPrefix);
47
+ }
48
+ } finally {
49
+ rmSync(pluginsDir, { force: true, recursive: true });
50
+ if (previousPluginsDir === undefined) {
51
+ delete process.env.CDXGEN_PLUGINS_DIR;
52
+ } else {
53
+ process.env.CDXGEN_PLUGINS_DIR = previousPluginsDir;
54
+ }
55
+ }
56
+ });
57
+ });
@@ -1,26 +1,203 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
- import { cdx_16, cdx_17 } from "@appthreat/cdx-proto";
4
3
  import {
5
- fromBinary,
6
- fromJsonString,
7
- toBinary,
8
- toJson,
9
- } from "@bufbuild/protobuf";
4
+ createBom,
5
+ decodeBomBinary,
6
+ decodeBomJson,
7
+ encodeBomBinary,
8
+ encodeBomJson,
9
+ parseBomBinary,
10
+ parseBomJson,
11
+ supportedSpecVersions,
12
+ } from "@appthreat/cdx-proto";
10
13
 
14
+ import { toCycloneDxSpecVersionString } from "./bomUtils.js";
11
15
  import { safeExistsSync, safeWriteSync } from "./utils.js";
12
16
 
17
+ const JSON_READ_OPTIONS = {
18
+ ignoreUnknownFields: true,
19
+ };
20
+
21
+ const BINARY_READ_OPTIONS = {
22
+ readUnknownFields: true,
23
+ };
24
+
25
+ const BINARY_WRITE_OPTIONS = {
26
+ writeUnknownFields: true,
27
+ };
28
+
29
+ const PROTO_BOM_FILE_EXTENSIONS = [".cdx", ".cdx.bin", ".proto"];
30
+
31
+ const DEFAULT_SPEC_VERSION =
32
+ supportedSpecVersions[supportedSpecVersions.length - 1];
33
+ const PROTO_SUPPORTED_SPEC_VERSIONS = new Set(
34
+ supportedSpecVersions.map((specVersion) =>
35
+ toCycloneDxSpecVersionString(specVersion),
36
+ ),
37
+ );
38
+
39
+ const isProtoMessageBom = (bom) =>
40
+ Boolean(
41
+ bom &&
42
+ typeof bom === "object" &&
43
+ !Array.isArray(bom) &&
44
+ typeof bom.$typeName === "string" &&
45
+ bom.specVersion,
46
+ );
47
+
48
+ const hasExplicitSpecVersion = (bomJson) =>
49
+ Boolean(
50
+ bomJson &&
51
+ typeof bomJson === "object" &&
52
+ !Array.isArray(bomJson) &&
53
+ (bomJson.specVersion !== undefined || bomJson.spec_version !== undefined),
54
+ );
55
+
56
+ const resolveExplicitSpecVersion = (bomJson) =>
57
+ bomJson?.specVersion ?? bomJson?.spec_version;
58
+
59
+ const hasProvidedSpecVersion = (specVersion) =>
60
+ specVersion !== undefined &&
61
+ specVersion !== null &&
62
+ `${specVersion}`.trim() !== "";
63
+
64
+ export const isProtoSupportedSpecVersion = (specVersion) => {
65
+ if (!hasProvidedSpecVersion(specVersion)) {
66
+ return true;
67
+ }
68
+ const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
69
+ return (
70
+ normalizedSpecVersion !== undefined &&
71
+ PROTO_SUPPORTED_SPEC_VERSIONS.has(normalizedSpecVersion)
72
+ );
73
+ };
74
+
75
+ export const assertProtoSupportedSpecVersion = (
76
+ specVersion,
77
+ operation = "protobuf operations",
78
+ ) => {
79
+ if (!hasProvidedSpecVersion(specVersion)) {
80
+ return;
81
+ }
82
+ const normalizedSpecVersion = toCycloneDxSpecVersionString(specVersion);
83
+ if (isProtoSupportedSpecVersion(specVersion)) {
84
+ return;
85
+ }
86
+ const displaySpecVersion = normalizedSpecVersion || `${specVersion}`.trim();
87
+ throw new Error(
88
+ `CycloneDX ${displaySpecVersion} is not currently supported for ${operation}. @appthreat/cdx-proto supports ${supportedSpecVersions.join(", ")} only.`,
89
+ );
90
+ };
91
+
92
+ const OBJECT_WRAPPED_LIST_FIELDS = ["declarations", "definitions"];
93
+
94
+ const isPlainObject = (value) =>
95
+ Boolean(value && typeof value === "object" && !Array.isArray(value));
96
+
97
+ const normalizeObjectWrappedListsForProto = (bomJson) => {
98
+ if (!isPlainObject(bomJson)) {
99
+ return bomJson;
100
+ }
101
+ const normalizedBomJson = { ...bomJson };
102
+ for (const fieldName of OBJECT_WRAPPED_LIST_FIELDS) {
103
+ if (isPlainObject(normalizedBomJson[fieldName])) {
104
+ normalizedBomJson[fieldName] = [normalizedBomJson[fieldName]];
105
+ }
106
+ }
107
+ return normalizedBomJson;
108
+ };
109
+
110
+ const mergeObjectWrappedListEntries = (entries) => {
111
+ const mergedEntry = {};
112
+ for (const entry of entries) {
113
+ if (!isPlainObject(entry)) {
114
+ continue;
115
+ }
116
+ for (const [key, value] of Object.entries(entry)) {
117
+ if (value === undefined) {
118
+ continue;
119
+ }
120
+ if (Array.isArray(value)) {
121
+ mergedEntry[key] = [...(mergedEntry[key] || []), ...value];
122
+ continue;
123
+ }
124
+ if (isPlainObject(value) && isPlainObject(mergedEntry[key])) {
125
+ mergedEntry[key] = { ...mergedEntry[key], ...value };
126
+ continue;
127
+ }
128
+ if (mergedEntry[key] === undefined) {
129
+ mergedEntry[key] = value;
130
+ }
131
+ }
132
+ }
133
+ return Object.keys(mergedEntry).length ? mergedEntry : undefined;
134
+ };
135
+
136
+ const normalizeObjectWrappedListsFromProto = (bomJson) => {
137
+ if (!isPlainObject(bomJson)) {
138
+ return bomJson;
139
+ }
140
+ const normalizedBomJson = { ...bomJson };
141
+ for (const fieldName of OBJECT_WRAPPED_LIST_FIELDS) {
142
+ if (!Array.isArray(normalizedBomJson[fieldName])) {
143
+ continue;
144
+ }
145
+ const mergedEntry = mergeObjectWrappedListEntries(
146
+ normalizedBomJson[fieldName],
147
+ );
148
+ if (mergedEntry) {
149
+ normalizedBomJson[fieldName] = mergedEntry;
150
+ } else {
151
+ delete normalizedBomJson[fieldName];
152
+ }
153
+ }
154
+ return normalizedBomJson;
155
+ };
156
+
157
+ const resolveBomMessage = (bomJson, specVersion = DEFAULT_SPEC_VERSION) => {
158
+ if (isProtoMessageBom(bomJson)) {
159
+ return bomJson;
160
+ }
161
+ if (typeof bomJson === "string" || bomJson instanceof String) {
162
+ const parsedBomJson = normalizeObjectWrappedListsForProto(
163
+ JSON.parse(`${bomJson}`),
164
+ );
165
+ if (hasExplicitSpecVersion(parsedBomJson)) {
166
+ assertProtoSupportedSpecVersion(
167
+ resolveExplicitSpecVersion(parsedBomJson),
168
+ "protobuf serialization",
169
+ );
170
+ return parseBomJson(parsedBomJson, JSON_READ_OPTIONS);
171
+ }
172
+ assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
173
+ return decodeBomJson(specVersion, parsedBomJson, JSON_READ_OPTIONS);
174
+ }
175
+ if (bomJson && typeof bomJson === "object" && !Array.isArray(bomJson)) {
176
+ const normalizedBomJson = normalizeObjectWrappedListsForProto(bomJson);
177
+ if (hasExplicitSpecVersion(normalizedBomJson)) {
178
+ assertProtoSupportedSpecVersion(
179
+ resolveExplicitSpecVersion(normalizedBomJson),
180
+ "protobuf serialization",
181
+ );
182
+ return parseBomJson(normalizedBomJson, JSON_READ_OPTIONS);
183
+ }
184
+ assertProtoSupportedSpecVersion(specVersion, "protobuf serialization");
185
+ return decodeBomJson(specVersion, normalizedBomJson, JSON_READ_OPTIONS);
186
+ }
187
+ return createBom(specVersion);
188
+ };
189
+
13
190
  /**
14
- * Stringify the given bom json based on the type.
191
+ * Determine whether a path looks like a CycloneDX protobuf file.
15
192
  *
16
- * @param {string | Object} bomJson string or object
17
- * @returns {string} BOM json string
193
+ * @param {string} filePath File path
194
+ * @returns {boolean} true when the path looks like a protobuf BOM file
18
195
  */
19
- const stringifyIfNeeded = (bomJson) => {
20
- if (typeof bomJson === "string" || bomJson instanceof String) {
21
- return bomJson;
22
- }
23
- return JSON.stringify(bomJson);
196
+ export const isProtoBomFile = (filePath) => {
197
+ const normalizedPath = `${filePath || ""}`.toLowerCase();
198
+ return PROTO_BOM_FILE_EXTENSIONS.some((extension) =>
199
+ normalizedPath.endsWith(extension),
200
+ );
24
201
  };
25
202
 
26
203
  /**
@@ -28,27 +205,16 @@ const stringifyIfNeeded = (bomJson) => {
28
205
  *
29
206
  * @param {string | Object} bomJson BOM Json
30
207
  * @param {string} binFile Binary file name
208
+ * @param {string | number} [specVersion] CycloneDX spec version fallback for BOMs without specVersion
31
209
  */
32
- export const writeBinary = (bomJson, binFile) => {
210
+ export const writeBinary = (
211
+ bomJson,
212
+ binFile,
213
+ specVersion = DEFAULT_SPEC_VERSION,
214
+ ) => {
33
215
  if (bomJson && binFile) {
34
- let bomSchema;
35
- if (+bomJson.specVersion === 1.7) {
36
- bomSchema = cdx_17.BomSchema;
37
- } else {
38
- bomSchema = cdx_16.BomSchema;
39
- }
40
- safeWriteSync(
41
- binFile,
42
- toBinary(
43
- bomSchema,
44
- fromJsonString(bomSchema, stringifyIfNeeded(bomJson), {
45
- ignoreUnknownFields: true,
46
- }),
47
- ),
48
- {
49
- writeUnknownFields: true,
50
- },
51
- );
216
+ const bomMessage = resolveBomMessage(bomJson, specVersion);
217
+ safeWriteSync(binFile, encodeBomBinary(bomMessage, BINARY_WRITE_OPTIONS));
52
218
  }
53
219
  };
54
220
 
@@ -57,23 +223,21 @@ export const writeBinary = (bomJson, binFile) => {
57
223
  *
58
224
  * @param {string} binFile Binary file name
59
225
  * @param {boolean} asJson Convert to JSON
60
- * @param {number} specVersion Specification version. Defaults to 1.7
226
+ * @param {string | number} [specVersion] Optional specification version. When omitted, cdxgen auto-detects the matching schema.
61
227
  */
62
- export const readBinary = (binFile, asJson = true, specVersion = 1.7) => {
228
+ export const readBinary = (binFile, asJson, specVersion) => {
229
+ asJson = asJson ?? true;
230
+ assertProtoSupportedSpecVersion(specVersion, "protobuf decoding");
63
231
  if (!safeExistsSync(binFile)) {
64
232
  return undefined;
65
233
  }
66
- let bomSchema;
67
- if (specVersion === 1.7) {
68
- bomSchema = cdx_17.BomSchema;
69
- } else {
70
- bomSchema = cdx_16.BomSchema;
71
- }
72
- const bomObject = fromBinary(bomSchema, readFileSync(binFile), {
73
- readUnknownFields: true,
74
- });
234
+ const binaryData = readFileSync(binFile);
235
+ const bomObject =
236
+ specVersion !== undefined && specVersion !== null && specVersion !== ""
237
+ ? decodeBomBinary(specVersion, binaryData, BINARY_READ_OPTIONS)
238
+ : parseBomBinary(binaryData, BINARY_READ_OPTIONS);
75
239
  if (asJson) {
76
- return toJson(bomSchema, bomObject, { emitDefaultValues: true });
240
+ return normalizeObjectWrappedListsFromProto(encodeBomJson(bomObject));
77
241
  }
78
242
  return bomObject;
79
243
  };