@cyclonedx/cdxgen 9.3.1 → 9.4.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.
package/README.md CHANGED
@@ -10,6 +10,12 @@ NOTE:
10
10
 
11
11
  CycloneDX 1.5 specification is brand new and unsupported by many downstream tools. Use version 8.6.0 for 1.4 compatibility or pass the argument `--spec-version 1.4`.
12
12
 
13
+ ## Why cdxgen?
14
+
15
+ A typical application might comprise of several repos, components, and libraries linked together. Traditional techniques to generate a single SBoM per language or package manifest do not work in enterprise environments. So we built cdxgen - the universal polyglot SBoM generator!
16
+
17
+ <img src="./docs/why-cdxgen.jpg" alt="why cdxgen" width="256">
18
+
13
19
  ## Supported languages and package format
14
20
 
15
21
  | Language/Platform | Package format | Transitive dependencies |
@@ -61,7 +67,7 @@ Footnotes:
61
67
  - [4] - See section on plugins
62
68
  - [5] - Powered by osquery. See section on plugins
63
69
 
64
- ![cdxgen tree](./docs/cdxgen-tree.jpg)
70
+ <img src="./docs/cdxgen-tree.jpg" alt="cdxgen tree" width="256">
65
71
 
66
72
  ### Automatic usage detection
67
73
 
@@ -289,7 +295,9 @@ cdxgen can retain the dependency tree under the `dependencies` attribute for a s
289
295
  | MAVEN_HOME | Specify maven home |
290
296
  | GRADLE_CACHE_DIR | Specify gradle cache directory. Useful for class name resolving |
291
297
  | GRADLE_MULTI_PROJECT_MODE | Unused. Automatically handled |
292
- | GRADLE_ARGS | Set to pass additional arguments such as profile or settings to gradle. Eg: --configuration runtimeClassPath |
298
+ | GRADLE_ARGS | Set to pass additional arguments such as profile or settings to gradle (all tasks). Eg: --configuration runtimeClassPath |
299
+ | GRADLE_ARGS_PROPERTIES | Set to pass additional arguments only to the `gradle properties` task, used for collecting metadata about the project |
300
+ | GRADLE_ARGS_DEPENDENCIES | Set to pass additional arguments only to the `gradle dependencies` task, used for listing actual project dependencies |
293
301
  | GRADLE_HOME | Specify gradle home |
294
302
  | GRADLE_CMD | Set to override gradle command |
295
303
  | GRADLE_DEPENDENCY_TASK | By default cdxgen use the task "dependencies" to collect packages. Set to override the task name. |
@@ -373,10 +381,33 @@ cdxgen can sign the generated SBoM json file to increase authenticity and non-re
373
381
  - SBOM_SIGN_PRIVATE_KEY: Location to the RSA private key
374
382
  - SBOM_SIGN_PUBLIC_KEY: Optional. Location to the RSA public key
375
383
 
376
- To generate test public/private key pairs, you can run cdxgen by passing the argument `--generate-key-and-sign`. The generated json file would have an attribute called `signature` which could be used for validation. [jwt.io](jwt.io) is a known site that could be used for such signature validation.
384
+ To generate test public/private key pairs, you can run cdxgen by passing the argument `--generate-key-and-sign`. The generated json file would have an attribute called `signature` which could be used for validation. [jwt.io](https://jwt.io) is a known site that could be used for such signature validation.
377
385
 
378
386
  ![SBoM signing](sbom-sign.jpg)
379
387
 
388
+ ### Verifying the signature (Node.js example)
389
+
390
+ There are many [libraries](https://jwt.io/#libraries-io) available to validate JSON Web Tokens. Below is a javascript example.
391
+
392
+ ```js
393
+ # npm install jws
394
+ const jws = require("jws");
395
+ const fs = require("fs");
396
+ // Location of the SBoM json file
397
+ const bomJsonFile = "bom.json";
398
+ // Location of the public key
399
+ const publicKeyFile = "public.key";
400
+ const bomJson = JSON.parse(fs.readFileSync(bomJsonFile, "utf8"));
401
+ // Retrieve the signature
402
+ const bomSignature = bomJson.signature.value;
403
+ const validationResult = jws.verify(bomSignature, bomJson.signature.algorithm, fs.readFileSync(publicKeyFile, "utf8"));
404
+ if (validationResult) {
405
+ console.log("Signature is valid!");
406
+ } else {
407
+ console.log("SBoM signature is invalid :(");
408
+ }
409
+ ```
410
+
380
411
  ## Automatic services detection
381
412
 
382
413
  cdxgen could automatically detect names of services from YAML manifests such as docker-compose or Kubernetes or Skaffold manifests. These would be populated under the `services` attribute in the generated SBoM. Please help improve this feature by filing issues for any inaccurate detection.
@@ -412,6 +443,50 @@ const bomNSData = await createBom(filePath, options);
412
443
  const dbody = await submitBom(args, bomNSData.bomJson);
413
444
  ```
414
445
 
446
+ ## Interactive mode
447
+
448
+ `cdxi` is a new interactive REPL server to interactively create, import and search an SBoM. All the exported functions from cdxgen and node.js could be used in this mode. In addition, several custom commands are defined.
449
+
450
+ ### Custom commands
451
+
452
+ | Command | Description |
453
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
454
+ | .create | Create an SBoM from a path |
455
+ | .import | Import an existing SBoM from a path. Any SBoM in CycloneDX format is supported. |
456
+ | .search | Search the given string in the components name, group, purl and description |
457
+ | .sort | Sort the components based on the given attribute. Eg: .sort name to sort by name. Accepts full jsonata [order by](http://docs.jsonata.org/path-operators#order-by-) clause too. Eg: `.sort components^(>name)` |
458
+ | .query | Pass a raw query in [jsonata](http://docs.jsonata.org/) format |
459
+ | .print | Print the SBoM as a table |
460
+ | .tree | Print the dependency tree if available |
461
+ | .validate | Validate the SBoM |
462
+ | .exit | To exit the shell |
463
+ | .save | To save the modified SBoM to a new file |
464
+ | .update | Update components based on query expression. Use syntax `\| query \| new object \|`. See example. |
465
+
466
+ ### Sample REPL usage
467
+
468
+ Start the REPL server.
469
+
470
+ ```shell
471
+ cdxi
472
+ ```
473
+
474
+ Below are some example commands to create an SBoM for a spring application and perform searches and queries.
475
+
476
+ ```
477
+ .create /mnt/work/vuln-spring
478
+ .print
479
+ .search spring
480
+ .query components[name ~> /spring/ and scope = "required"]
481
+ .sort name
482
+ .sort components^(>name)
483
+ .update | components[name ~> /spring/] | {'publisher': "foo"} |
484
+ ```
485
+
486
+ ### REPL History
487
+
488
+ Repl history will get persisted under `$HOME/.config/.cdxgen` directory. To override this location, use the environment variable `CDXGEN_REPL_HISTORY`.
489
+
415
490
  ## Node.js >= 20 permission model
416
491
 
417
492
  Refer to the [permissions document](./docs/PERMISSIONS.md)
@@ -425,3 +500,7 @@ npm run lint
425
500
  npm run pretty
426
501
  npm test
427
502
  ```
503
+
504
+ ## Enterprise support
505
+
506
+ Enterprise support including custom development and integration services are available via AppThreat Ltd. Free community support is also available via [discord](https://discord.gg/tmmtjCEHNV).
package/bin/cdxgen.js CHANGED
@@ -266,9 +266,10 @@ const checkPermissions = (filePath) => {
266
266
  }
267
267
  let privateKeyToUse = undefined;
268
268
  let jwkPublicKey = undefined;
269
+ let publicKeyFile = undefined;
269
270
  if (args.generateKeyAndSign) {
270
271
  const jdirName = dirname(jsonFile);
271
- const publicKeyFile = join(jdirName, "public.key");
272
+ publicKeyFile = join(jdirName, "public.key");
272
273
  const privateKeyFile = join(jdirName, "private.key");
273
274
  const { privateKey, publicKey } = crypto.generateKeyPairSync(
274
275
  "rsa",
@@ -331,6 +332,26 @@ const checkPermissions = (filePath) => {
331
332
  jsonFile,
332
333
  JSON.stringify(bomJsonUnsignedObj, null, 2)
333
334
  );
335
+ if (publicKeyFile) {
336
+ // Verifying this signature
337
+ const signatureVerification = jws.verify(
338
+ signature,
339
+ alg,
340
+ fs.readFileSync(publicKeyFile, "utf8")
341
+ );
342
+ if (signatureVerification) {
343
+ console.log(
344
+ "SBoM signature is verifiable with the public key and the algorithm",
345
+ publicKeyFile,
346
+ alg
347
+ );
348
+ } else {
349
+ console.log("SBoM signature verification was unsuccessful");
350
+ console.log(
351
+ "Check if the public key was exported in PEM format"
352
+ );
353
+ }
354
+ }
334
355
  }
335
356
  } catch (ex) {
336
357
  console.log("SBoM signing was unsuccessful", ex);
package/bin/repl.js ADDED
@@ -0,0 +1,311 @@
1
+ import repl from "node:repl";
2
+ import jsonata from "jsonata";
3
+ import fs from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import process from "node:process";
7
+
8
+ import { createBom } from "../index.js";
9
+ import { validateBom } from "../validator.js";
10
+ import { printTable, printDependencyTree } from "../display.js";
11
+
12
+ const options = {
13
+ useColors: true,
14
+ breakEvalOnSigint: true,
15
+ preview: true,
16
+ prompt: "↝ ",
17
+ ignoreUndefined: true,
18
+ useGlobal: true
19
+ };
20
+
21
+ const cdxArt = ` ██████╗██████╗ ██╗ ██╗
22
+ ██╔════╝██╔══██╗╚██╗██╔╝
23
+ ██║ ██║ ██║ ╚███╔╝
24
+ ██║ ██║ ██║ ██╔██╗
25
+ ╚██████╗██████╔╝██╔╝ ██╗
26
+ ╚═════╝╚═════╝ ╚═╝ ╚═╝
27
+ `;
28
+
29
+ console.log("\n" + cdxArt);
30
+
31
+ // The current sbom is stored here
32
+ let sbom = undefined;
33
+
34
+ let historyFile = undefined;
35
+ const historyConfigDir = join(homedir(), ".config", ".cdxgen");
36
+ if (!process.env.CDXGEN_REPL_HISTORY && !fs.existsSync(historyConfigDir)) {
37
+ try {
38
+ fs.mkdirSync(historyConfigDir, { recursive: true });
39
+ historyFile = join(historyConfigDir, ".repl_history");
40
+ } catch (e) {
41
+ // ignore
42
+ }
43
+ } else {
44
+ historyFile = join(historyConfigDir, ".repl_history");
45
+ }
46
+
47
+ export const importSbom = (sbomOrPath) => {
48
+ if (sbomOrPath && sbomOrPath.endsWith(".json") && fs.existsSync(sbomOrPath)) {
49
+ try {
50
+ sbom = JSON.parse(fs.readFileSync(sbomOrPath, "utf-8"));
51
+ console.log(`✅ SBoM imported successfully from ${sbomOrPath}`);
52
+ } catch (e) {
53
+ console.log(
54
+ `⚠ Unable to import the SBoM from ${sbomOrPath} due to ${e}`
55
+ );
56
+ }
57
+ } else {
58
+ console.log(`⚠ ${sbomOrPath} is invalid.`);
59
+ }
60
+ };
61
+ // Load any sbom passed from the command line
62
+ if (process.argv.length > 2) {
63
+ importSbom(process.argv[process.argv.length - 1]);
64
+ console.log("💭 Type .print to view the SBoM as a table");
65
+ } else if (fs.existsSync("bom.json")) {
66
+ // If the current directory has a bom.json load it
67
+ importSbom("bom.json");
68
+ } else {
69
+ console.log("💭 Use .create <path> to create an SBoM for the given path.");
70
+ console.log("💭 Use .import <json> to import an existing SBoM.");
71
+ console.log("💭 Type .exit or press ctrl+d to close.");
72
+ }
73
+
74
+ const cdxgenRepl = repl.start(options);
75
+ if (historyFile) {
76
+ cdxgenRepl.setupHistory(
77
+ process.env.CDXGEN_REPL_HISTORY || historyFile,
78
+ (err) => {
79
+ if (err) {
80
+ console.log(
81
+ "⚠ REPL history would not be persisted for this session. Set the environment variable CDXGEN_REPL_HISTORY to specify a custom history file"
82
+ );
83
+ }
84
+ }
85
+ );
86
+ }
87
+ cdxgenRepl.defineCommand("create", {
88
+ help: "create an SBoM for the given path",
89
+ async action(sbomOrPath) {
90
+ this.clearBufferedCommand();
91
+ const tempDir = fs.mkdtempSync(join(tmpdir(), "cdxgen-repl-"));
92
+ const bomFile = join(tempDir, "bom.json");
93
+ const bomNSData = await createBom(sbomOrPath, {
94
+ multiProject: true,
95
+ installDeps: true,
96
+ output: bomFile
97
+ });
98
+ if (bomNSData) {
99
+ sbom = bomNSData.bomJson;
100
+ console.log("✅ SBoM imported successfully.");
101
+ console.log("💭 Type .print to view the SBoM as a table");
102
+ } else {
103
+ console.log("SBoM was not generated successfully");
104
+ }
105
+ this.displayPrompt();
106
+ }
107
+ });
108
+ cdxgenRepl.defineCommand("import", {
109
+ help: "import an existing SBoM",
110
+ action(sbomOrPath) {
111
+ this.clearBufferedCommand();
112
+ importSbom(sbomOrPath);
113
+ this.displayPrompt();
114
+ }
115
+ });
116
+ cdxgenRepl.defineCommand("exit", {
117
+ help: "exit",
118
+ action() {
119
+ this.close();
120
+ }
121
+ });
122
+ cdxgenRepl.defineCommand("sbom", {
123
+ help: "show the current sbom",
124
+ action() {
125
+ if (sbom) {
126
+ console.log(sbom);
127
+ } else {
128
+ console.log(
129
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
130
+ );
131
+ }
132
+ this.displayPrompt();
133
+ }
134
+ });
135
+ cdxgenRepl.defineCommand("search", {
136
+ help: "search the current sbom",
137
+ async action(searchStr) {
138
+ if (sbom) {
139
+ if (searchStr) {
140
+ try {
141
+ if (!searchStr.includes("~>")) {
142
+ searchStr = `components[group ~> /${searchStr}/i or name ~> /${searchStr}/i or description ~> /${searchStr}/i or publisher ~> /${searchStr}/i or purl ~> /${searchStr}/i]`;
143
+ }
144
+ const expression = jsonata(searchStr);
145
+ let components = await expression.evaluate(sbom);
146
+ if (!components) {
147
+ console.log("No results found!");
148
+ } else {
149
+ printTable({ components, dependencies: [] });
150
+ }
151
+ } catch (e) {
152
+ console.log(e);
153
+ }
154
+ } else {
155
+ console.log(
156
+ "⚠ Specify the search string. Eg: .search <search string>"
157
+ );
158
+ }
159
+ } else {
160
+ console.log(
161
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
162
+ );
163
+ }
164
+ this.displayPrompt();
165
+ }
166
+ });
167
+ cdxgenRepl.defineCommand("sort", {
168
+ help: "sort the current sbom based on the attribute",
169
+ async action(sortStr) {
170
+ if (sbom) {
171
+ if (sortStr) {
172
+ try {
173
+ if (!sortStr.includes("^")) {
174
+ sortStr = `components^(${sortStr})`;
175
+ }
176
+ const expression = jsonata(sortStr);
177
+ let components = await expression.evaluate(sbom);
178
+ if (!components) {
179
+ console.log("No results found!");
180
+ } else {
181
+ printTable({ components, dependencies: [] });
182
+ // Store the sorted list in memory
183
+ if (components.length === sbom.components.length) {
184
+ sbom.components = components;
185
+ }
186
+ }
187
+ } catch (e) {
188
+ console.log(e);
189
+ }
190
+ } else {
191
+ console.log("⚠ Specify the attribute to sort by. Eg: .sort name");
192
+ }
193
+ } else {
194
+ console.log(
195
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
196
+ );
197
+ }
198
+ this.displayPrompt();
199
+ }
200
+ });
201
+ cdxgenRepl.defineCommand("query", {
202
+ help: "query the current sbom",
203
+ async action(querySpec) {
204
+ if (sbom) {
205
+ if (querySpec) {
206
+ try {
207
+ const expression = jsonata(querySpec);
208
+ console.log(await expression.evaluate(sbom));
209
+ } catch (e) {
210
+ console.log(e);
211
+ }
212
+ } else {
213
+ console.log(
214
+ "⚠ Specify the search specification in jsonata format. Eg: .query metadata.component"
215
+ );
216
+ }
217
+ } else {
218
+ console.log(
219
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
220
+ );
221
+ }
222
+ this.displayPrompt();
223
+ }
224
+ });
225
+ cdxgenRepl.defineCommand("print", {
226
+ help: "print the current sbom as a table",
227
+ action() {
228
+ if (sbom) {
229
+ printTable(sbom);
230
+ } else {
231
+ console.log(
232
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
233
+ );
234
+ }
235
+ this.displayPrompt();
236
+ }
237
+ });
238
+ cdxgenRepl.defineCommand("tree", {
239
+ help: "display the dependency tree",
240
+ action() {
241
+ if (sbom) {
242
+ printDependencyTree(sbom);
243
+ } else {
244
+ console.log(
245
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
246
+ );
247
+ }
248
+ this.displayPrompt();
249
+ }
250
+ });
251
+ cdxgenRepl.defineCommand("validate", {
252
+ help: "validate the sbom",
253
+ action() {
254
+ if (sbom) {
255
+ const result = validateBom(sbom);
256
+ if (result) {
257
+ console.log("SBoM is valid!");
258
+ }
259
+ } else {
260
+ console.log(
261
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
262
+ );
263
+ }
264
+ this.displayPrompt();
265
+ }
266
+ });
267
+ cdxgenRepl.defineCommand("save", {
268
+ help: "save the sbom to a new file",
269
+ action(saveToFile) {
270
+ if (sbom) {
271
+ if (!saveToFile) {
272
+ saveToFile = "bom.json";
273
+ }
274
+ fs.writeFileSync(saveToFile, JSON.stringify(sbom, null, 2));
275
+ console.log(`SBoM saved successfully to ${saveToFile}`);
276
+ } else {
277
+ console.log(
278
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
279
+ );
280
+ }
281
+ this.displayPrompt();
282
+ }
283
+ });
284
+ cdxgenRepl.defineCommand("update", {
285
+ help: "update the sbom components based on the given query",
286
+ async action(updateSpec) {
287
+ if (sbom) {
288
+ if (!updateSpec) {
289
+ return;
290
+ }
291
+ if (!updateSpec.startsWith("|")) {
292
+ updateSpec = "|" + updateSpec;
293
+ }
294
+ if (!updateSpec.endsWith("|")) {
295
+ updateSpec = updateSpec + "|";
296
+ }
297
+ updateSpec = "$ ~> " + updateSpec;
298
+ const expression = jsonata(updateSpec);
299
+ const newSbom = await expression.evaluate(sbom);
300
+ if (newSbom && newSbom.components.length <= sbom.components.length) {
301
+ sbom = newSbom;
302
+ }
303
+ console.log("SBoM updated successfully.");
304
+ } else {
305
+ console.log(
306
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
307
+ );
308
+ }
309
+ this.displayPrompt();
310
+ }
311
+ });
package/display.js CHANGED
@@ -13,6 +13,9 @@ const MAX_TREE_DEPTH = 3;
13
13
 
14
14
  export const printTable = (bomJson) => {
15
15
  const data = [["Group", "Name", "Version", "Scope"]];
16
+ if (!bomJson || !bomJson.components) {
17
+ return;
18
+ }
16
19
  for (const comp of bomJson.components) {
17
20
  data.push([comp.group || "", comp.name, comp.version, comp.scope || ""]);
18
21
  }
package/docker.js CHANGED
@@ -24,7 +24,7 @@ import { x } from "tar";
24
24
  import { spawnSync } from "node:child_process";
25
25
  import { DEBUG_MODE } from "./utils.js";
26
26
 
27
- const isWin = _platform() === "win32";
27
+ export const isWin = _platform() === "win32";
28
28
 
29
29
  let dockerConn = undefined;
30
30
  let isPodman = false;
package/docker.test.js CHANGED
@@ -3,14 +3,17 @@ import {
3
3
  parseImageName,
4
4
  getImage,
5
5
  removeImage,
6
- exportImage
6
+ exportImage,
7
+ isWin
7
8
  } from "./docker.js";
8
9
  import { expect, test } from "@jest/globals";
9
10
 
10
11
  test("docker connection", async () => {
11
- const dockerConn = await getConnection();
12
- expect(dockerConn);
13
- });
12
+ if (!(isWin && process.env.CI === "true")) {
13
+ const dockerConn = await getConnection();
14
+ expect(dockerConn);
15
+ }
16
+ }, 120000);
14
17
 
15
18
  test("parseImageName tests", () => {
16
19
  expect(parseImageName("debian")).toEqual({
@@ -59,7 +62,7 @@ test("parseImageName tests", () => {
59
62
  digest: "5d008306a7c5d09ba0161a3408fa3839dc2c9dd991ffb68adecc1040399fe9e1",
60
63
  platform: ""
61
64
  });
62
- });
65
+ }, 120000);
63
66
 
64
67
  test("docker getImage", async () => {
65
68
  const imageData = await getImage("hello-world:latest");