@cyclonedx/cdxgen 9.4.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  ![cdxgen logo](cdxgen.png)
4
4
 
5
- cdxgen is a cli tool and a library to create a valid and compliant [CycloneDX][cyclonedx-homepage] Software Bill-of-Materials (SBOM) containing an aggregate of all project dependencies for c/c++, node.js, php, python, ruby, rust, java, .Net, dart, haskell, elixir, and Go projects in JSON format. CycloneDX 1.5 is a lightweight SBOM specification that is easily created, human and machine-readable, and simple to parse.
5
+ cdxgen is a cli tool, library, [REPL](./ADVANCED.md) and server to create a valid and compliant [CycloneDX][cyclonedx-homepage] Software Bill-of-Materials (SBOM) containing an aggregate of all project dependencies for c/c++, node.js, php, python, ruby, rust, java, .Net, dart, haskell, elixir, and Go projects in JSON format. CycloneDX 1.5 is a lightweight SBOM specification that is easily created, human and machine-readable, and simple to parse.
6
6
 
7
- When used with plugins, cdxgen could generate an SBoM for Linux docker images and even VMs running Linux or Windows operating system.
7
+ When used with plugins, cdxgen could generate an SBoM for Linux docker images and even VMs running Linux or Windows operating system. cdxgen also includes a tool called `evinse` that can generate component evidences for some languages.
8
8
 
9
9
  NOTE:
10
10
 
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`.
11
+ CycloneDX 1.5 specification is 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
13
  ## Why cdxgen?
14
14
 
@@ -93,7 +93,7 @@ sudo npm install -g @cyclonedx/cdxgen@8.6.0
93
93
  Deno install is also supported.
94
94
 
95
95
  ```shell
96
- deno install --allow-read --allow-env --allow-run --allow-sys=uid,systemMemoryInfo --allow-write --allow-net -n cdxgen "npm:@cyclonedx/cdxgen"
96
+ deno install --allow-read --allow-env --allow-run --allow-sys=uid,systemMemoryInfo --allow-write --allow-net -n cdxgen "npm:@cyclonedx/cdxgen/cdxgen"
97
97
  ```
98
98
 
99
99
  You can also use the cdxgen container image
@@ -373,6 +373,10 @@ cdxgen -t os
373
373
 
374
374
  This feature is powered by osquery which is [installed](https://github.com/cyclonedx/cdxgen-plugins-bin/blob/main/build.sh#L8) along with the binary plugins. cdxgen would opportunistically try to detect as many components, apps and extensions as possible using the [default queries](queries.json). The process would take several minutes and result in an SBoM file with thousands of components.
375
375
 
376
+ ## Generating component evidence
377
+
378
+ See [evinse mode](./ADVANCED.md) in the advanced documentation.
379
+
376
380
  ## SBoM signing
377
381
 
378
382
  cdxgen can sign the generated SBoM json file to increase authenticity and non-repudiation capabilities. To enable this, set the following environment variables.
@@ -385,7 +389,16 @@ To generate test public/private key pairs, you can run cdxgen by passing the arg
385
389
 
386
390
  ![SBoM signing](sbom-sign.jpg)
387
391
 
388
- ### Verifying the signature (Node.js example)
392
+ ### Verifying the signature
393
+
394
+ Use the bundled `cdx-verify` command which supports verifying a single signature added at the bom level.
395
+
396
+ ```shell
397
+ npm install -g @cyclonedx/cdxgen
398
+ cdx-verify -i bom.json --public-key public.key
399
+ ```
400
+
401
+ ### Custom verification tool (Node.js example)
389
402
 
390
403
  There are many [libraries](https://jwt.io/#libraries-io) available to validate JSON Web Tokens. Below is a javascript example.
391
404
 
@@ -410,7 +423,7 @@ if (validationResult) {
410
423
 
411
424
  ## Automatic services detection
412
425
 
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.
426
+ cdxgen can 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. With [evinse](./ADVANCED.md), additional services could be detected by parsing common annotations from the source code.
414
427
 
415
428
  ## Conversion to SPDX format
416
429
 
@@ -443,50 +456,6 @@ const bomNSData = await createBom(filePath, options);
443
456
  const dbody = await submitBom(args, bomNSData.bomJson);
444
457
  ```
445
458
 
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
-
490
459
  ## Node.js >= 20 permission model
491
460
 
492
461
  Refer to the [permissions document](./docs/PERMISSIONS.md)
package/analyzer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parse } from "@babel/parser";
2
- import babelTraverse from "@babel/traverse";
2
+ import traverse from "@babel/traverse";
3
3
  import { join } from "path";
4
4
  import { readdirSync, statSync, readFileSync } from "fs";
5
5
  import { basename, resolve, isAbsolute } from "path";
@@ -23,7 +23,7 @@ const IGNORE_DIRS = [
23
23
  ];
24
24
 
25
25
  const IGNORE_FILE_PATTERN = new RegExp(
26
- "(conf|test|spec|mock|\\.d)\\.(js|ts|tsx)$",
26
+ "(conf|config|test|spec|mock|\\.d)\\.(js|ts|tsx)$",
27
27
  "i"
28
28
  );
29
29
 
@@ -64,6 +64,7 @@ const babelParserOptions = {
64
64
  sourceType: "unambiguous",
65
65
  allowImportExportEverywhere: true,
66
66
  allowAwaitOutsideFunction: true,
67
+ allowNewTargetOutsideFunction: true,
67
68
  allowReturnOutsideFunction: true,
68
69
  allowSuperOutsideMethod: true,
69
70
  errorRecovery: true,
@@ -123,8 +124,32 @@ const setFileRef = (allImports, file, pathway) => {
123
124
  */
124
125
  const parseFileASTTree = (file, allImports) => {
125
126
  const ast = parse(readFileSync(file, "utf-8"), babelParserOptions);
126
- babelTraverse(ast, {
127
- // Used for all ES6 import statements
127
+ traverse.default(ast, {
128
+ Import: (path) => {
129
+ if (path && path.node) {
130
+ setFileRef(allImports, file, path.node.source.value);
131
+ }
132
+ },
133
+ ImportDefaultSpecifier: (path) => {
134
+ if (path && path.node) {
135
+ setFileRef(allImports, file, path.node.source.value);
136
+ }
137
+ },
138
+ ImportNamespaceSpecifier: (path) => {
139
+ if (path && path.node) {
140
+ setFileRef(allImports, file, path.node.source.value);
141
+ }
142
+ },
143
+ ImportAttribute: (path) => {
144
+ if (path && path.node) {
145
+ setFileRef(allImports, file, path.node.source.value);
146
+ }
147
+ },
148
+ ImportOrExportDeclaration: (path) => {
149
+ if (path && path.node) {
150
+ setFileRef(allImports, file, path.node.source.value);
151
+ }
152
+ },
128
153
  ImportDeclaration: (path) => {
129
154
  if (path && path.node) {
130
155
  setFileRef(allImports, file, path.node.source.value);
package/bin/evinse.js ADDED
@@ -0,0 +1,121 @@
1
+ // Evinse (Evinse Verification Is Nearly SBoM Evidence)
2
+ import yargs from "yargs";
3
+ import { hideBin } from "yargs/helpers";
4
+ import { join } from "node:path";
5
+ import fs from "node:fs";
6
+ import { homedir, platform as _platform } from "node:os";
7
+ import process from "node:process";
8
+ import { analyzeProject, createEvinseFile, prepareDB } from "../evinser.js";
9
+ import { validateBom } from "../validator.js";
10
+ import { printCallStack, printOccurrences, printServices } from "../display.js";
11
+
12
+ const isWin = _platform() === "win32";
13
+ const isMac = _platform() === "darwin";
14
+ let ATOM_DB = join(homedir(), ".local", "share", ".atomdb");
15
+ if (isWin) {
16
+ ATOM_DB = join(homedir(), "AppData", "Local", ".atomdb");
17
+ } else if (isMac) {
18
+ ATOM_DB = join(homedir(), "Library", "Application Support", ".atomdb");
19
+ }
20
+
21
+ if (!process.env.ATOM_DB && !fs.existsSync(ATOM_DB)) {
22
+ try {
23
+ fs.mkdirSync(ATOM_DB, { recursive: true });
24
+ } catch (e) {
25
+ // ignore
26
+ }
27
+ }
28
+ const args = yargs(hideBin(process.argv))
29
+ .option("input", {
30
+ alias: "i",
31
+ description: "Input SBoM file. Default bom.json",
32
+ default: "bom.json"
33
+ })
34
+ .option("output", {
35
+ alias: "o",
36
+ description: "Output file. Default bom.evinse.json",
37
+ default: "bom.evinse.json"
38
+ })
39
+ .option("language", {
40
+ alias: "l",
41
+ description: "Application language",
42
+ default: "java",
43
+ choices: ["java", "jar", "javascript", "python", "android", "cpp"]
44
+ })
45
+ .option("db-path", {
46
+ description: `Atom slices DB path. Default ${ATOM_DB}`,
47
+ default: process.env.ATOM_DB || ATOM_DB
48
+ })
49
+ .option("force", {
50
+ description: "Force creation of the database",
51
+ default: false,
52
+ type: "boolean"
53
+ })
54
+ .option("skip-maven-collector", {
55
+ description:
56
+ "Skip collecting jars from maven and gradle caches. Can speedup re-runs if the data was cached previously.",
57
+ default: false,
58
+ type: "boolean"
59
+ })
60
+ .option("with-deep-jar-collector", {
61
+ description:
62
+ "Enable collection of all jars from maven cache directory. Useful to improve the recall for callstack evidence.",
63
+ default: false,
64
+ type: "boolean"
65
+ })
66
+ .option("annotate", {
67
+ description: "Include contents of atom slices as annotations",
68
+ default: true,
69
+ type: "boolean"
70
+ })
71
+ .option("with-data-flow", {
72
+ description: "Enable inter-procedural data-flow slicing.",
73
+ default: false,
74
+ type: "boolean"
75
+ })
76
+ .option("usages-slices-file", {
77
+ description: "Use an existing usages slices file.",
78
+ default: "usages.slices.json"
79
+ })
80
+ .option("data-flow-slices-file", {
81
+ description: "Use an existing data-flow slices file.",
82
+ default: "data-flow.slices.json"
83
+ })
84
+ .option("print", {
85
+ alias: "p",
86
+ type: "boolean",
87
+ description: "Print the evidences as table"
88
+ })
89
+ .scriptName("evinse")
90
+ .version()
91
+ .help("h").argv;
92
+
93
+ const evinseArt = `
94
+ ███████╗██╗ ██╗██╗███╗ ██╗███████╗███████╗
95
+ ██╔════╝██║ ██║██║████╗ ██║██╔════╝██╔════╝
96
+ █████╗ ██║ ██║██║██╔██╗ ██║███████╗█████╗
97
+ ██╔══╝ ╚██╗ ██╔╝██║██║╚██╗██║╚════██║██╔══╝
98
+ ███████╗ ╚████╔╝ ██║██║ ╚████║███████║███████╗
99
+ ╚══════╝ ╚═══╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
100
+ `;
101
+
102
+ console.log(evinseArt);
103
+ (async () => {
104
+ // First, prepare the database by cataloging jars and other libraries
105
+ const dbObjMap = await prepareDB(args);
106
+ if (dbObjMap) {
107
+ // Analyze the project using atom. Convert package namespaces to purl using the db
108
+ const sliceArtefacts = await analyzeProject(dbObjMap, args);
109
+ // Create the SBoM with Evidence
110
+ const bomJson = createEvinseFile(sliceArtefacts, args);
111
+ // Validate our final SBoM
112
+ if (!validateBom(bomJson)) {
113
+ process.exit(1);
114
+ }
115
+ if (args.print) {
116
+ printOccurrences(bomJson);
117
+ printCallStack(bomJson);
118
+ printServices(bomJson);
119
+ }
120
+ }
121
+ })();
package/bin/repl.js CHANGED
@@ -7,18 +7,28 @@ import process from "node:process";
7
7
 
8
8
  import { createBom } from "../index.js";
9
9
  import { validateBom } from "../validator.js";
10
- import { printTable, printDependencyTree } from "../display.js";
10
+ import {
11
+ printCallStack,
12
+ printOccurrences,
13
+ printTable,
14
+ printDependencyTree,
15
+ printServices
16
+ } from "../display.js";
11
17
 
12
18
  const options = {
13
19
  useColors: true,
14
20
  breakEvalOnSigint: true,
15
21
  preview: true,
16
- prompt: "↝ ",
22
+ prompt: "cdx ↝ ",
17
23
  ignoreUndefined: true,
18
24
  useGlobal: true
19
25
  };
20
26
 
21
- const cdxArt = ` ██████╗██████╗ ██╗ ██╗
27
+ // Use canonical terminal settings to support custom readlines
28
+ process.env.NODE_NO_READLINE = 1;
29
+
30
+ const cdxArt = `
31
+ ██████╗██████╗ ██╗ ██╗
22
32
  ██╔════╝██╔══██╗╚██╗██╔╝
23
33
  ██║ ██║ ██║ ╚███╔╝
24
34
  ██║ ██║ ██║ ██╔██╗
@@ -26,7 +36,7 @@ const cdxArt = ` ██████╗██████╗ ██╗ ██╗
26
36
  ╚═════╝╚═════╝ ╚═╝ ╚═╝
27
37
  `;
28
38
 
29
- console.log("\n" + cdxArt);
39
+ console.log(cdxArt);
30
40
 
31
41
  // The current sbom is stored here
32
42
  let sbom = undefined;
@@ -133,7 +143,7 @@ cdxgenRepl.defineCommand("sbom", {
133
143
  }
134
144
  });
135
145
  cdxgenRepl.defineCommand("search", {
136
- help: "search the current sbom",
146
+ help: "search the current sbom. performs case insensitive search on various attributes.",
137
147
  async action(searchStr) {
138
148
  if (sbom) {
139
149
  if (searchStr) {
@@ -199,7 +209,7 @@ cdxgenRepl.defineCommand("sort", {
199
209
  }
200
210
  });
201
211
  cdxgenRepl.defineCommand("query", {
202
- help: "query the current sbom",
212
+ help: "query the current sbom using jsonata expression",
203
213
  async action(querySpec) {
204
214
  if (sbom) {
205
215
  if (querySpec) {
@@ -249,7 +259,7 @@ cdxgenRepl.defineCommand("tree", {
249
259
  }
250
260
  });
251
261
  cdxgenRepl.defineCommand("validate", {
252
- help: "validate the sbom",
262
+ help: "validate the sbom using jsonschema",
253
263
  action() {
254
264
  if (sbom) {
255
265
  const result = validateBom(sbom);
@@ -309,3 +319,107 @@ cdxgenRepl.defineCommand("update", {
309
319
  this.displayPrompt();
310
320
  }
311
321
  });
322
+ cdxgenRepl.defineCommand("occurrences", {
323
+ help: "view components with evidence.occurrences",
324
+ async action() {
325
+ if (sbom) {
326
+ try {
327
+ const expression = jsonata(
328
+ "components[$count(evidence.occurrences) > 0]"
329
+ );
330
+ let components = await expression.evaluate(sbom);
331
+ if (!components) {
332
+ console.log(
333
+ "No results found. Use evinse command to generate an SBoM with evidence."
334
+ );
335
+ } else {
336
+ if (!Array.isArray(components)) {
337
+ components = [components];
338
+ }
339
+ printOccurrences({ components });
340
+ }
341
+ } catch (e) {
342
+ console.log(e);
343
+ }
344
+ } else {
345
+ console.log(
346
+ "⚠ No SBoM is loaded. Use .import command to import an evinse SBoM"
347
+ );
348
+ }
349
+ this.displayPrompt();
350
+ }
351
+ });
352
+ cdxgenRepl.defineCommand("discord", {
353
+ help: "display the discord invite link for support",
354
+ async action() {
355
+ console.log("Head to https://discord.gg/pF4BYWEJcS for support");
356
+ this.displayPrompt();
357
+ }
358
+ });
359
+ cdxgenRepl.defineCommand("sponsor", {
360
+ help: "display the sponsorship link to fund this project",
361
+ async action() {
362
+ console.log(
363
+ "Hey, thanks a lot for considering! https://github.com/sponsors/prabhu"
364
+ );
365
+ this.displayPrompt();
366
+ }
367
+ });
368
+ cdxgenRepl.defineCommand("callstack", {
369
+ help: "view components with evidence.callstack",
370
+ async action() {
371
+ if (sbom) {
372
+ try {
373
+ const expression = jsonata(
374
+ "components[$count(evidence.callstack.frames) > 0]"
375
+ );
376
+ let components = await expression.evaluate(sbom);
377
+ if (!components) {
378
+ console.log(
379
+ "callstack evidence was not found. Use evinse command to generate an SBoM with evidence."
380
+ );
381
+ } else {
382
+ if (!Array.isArray(components)) {
383
+ components = [components];
384
+ }
385
+ printCallStack({ components });
386
+ }
387
+ } catch (e) {
388
+ console.log(e);
389
+ }
390
+ } else {
391
+ console.log(
392
+ "⚠ No SBoM is loaded. Use .import command to import an evinse SBoM"
393
+ );
394
+ }
395
+ this.displayPrompt();
396
+ }
397
+ });
398
+ cdxgenRepl.defineCommand("services", {
399
+ help: "view services",
400
+ async action() {
401
+ if (sbom) {
402
+ try {
403
+ const expression = jsonata("services");
404
+ let services = await expression.evaluate(sbom);
405
+ if (!services) {
406
+ console.log(
407
+ "No services found. Use evinse command to generate an SBoM with evidence."
408
+ );
409
+ } else {
410
+ if (!Array.isArray(services)) {
411
+ services = [services];
412
+ }
413
+ printServices({ services });
414
+ }
415
+ } catch (e) {
416
+ console.log(e);
417
+ }
418
+ } else {
419
+ console.log(
420
+ "⚠ No SBoM is loaded. Use .import command to import an evinse SBoM"
421
+ );
422
+ }
423
+ this.displayPrompt();
424
+ }
425
+ });
package/bin/verify.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import fs from "node:fs";
6
+ import jws from "jws";
7
+ import process from "node:process";
8
+
9
+ const args = yargs(hideBin(process.argv))
10
+ .option("input", {
11
+ alias: "i",
12
+ default: "bom.json",
13
+ description: "Input json to validate. Default bom.json"
14
+ })
15
+ .option("public-key", {
16
+ default: "public.key",
17
+ description: "Public key in PEM format. Default public.key"
18
+ })
19
+ .scriptName("cdx-verify")
20
+ .version()
21
+ .help("h").argv;
22
+
23
+ const bomJson = JSON.parse(fs.readFileSync(args.input, "utf8"));
24
+ const bomSignature = bomJson?.signature?.value;
25
+ if (!bomSignature) {
26
+ console.log("No signature was found!");
27
+ } else {
28
+ const validationResult = jws.verify(
29
+ bomSignature,
30
+ bomJson.signature.algorithm,
31
+ fs.readFileSync(args.publicKey, "utf8")
32
+ );
33
+ if (validationResult) {
34
+ console.log("Signature is valid!");
35
+ } else {
36
+ console.log("SBoM signature is invalid!");
37
+ process.exit(1);
38
+ }
39
+ }
package/db.js ADDED
@@ -0,0 +1,80 @@
1
+ import path from "node:path";
2
+ import { Sequelize, DataTypes, Model } from "sequelize";
3
+ import SQLite from "sqlite3";
4
+
5
+ class Namespaces extends Model {}
6
+ class Usages extends Model {}
7
+ class DataFlows extends Model {}
8
+
9
+ export const createOrLoad = async (dbName, dbPath, logging = false) => {
10
+ const sequelize = new Sequelize({
11
+ define: {
12
+ freezeTableName: true
13
+ },
14
+ dialect: "sqlite",
15
+ dialectOptions: {
16
+ mode:
17
+ SQLite.OPEN_READWRITE |
18
+ SQLite.OPEN_CREATE |
19
+ SQLite.OPEN_NOMUTEX |
20
+ SQLite.OPEN_SHAREDCACHE
21
+ },
22
+ storage: dbPath.includes("memory") ? dbPath : path.join(dbPath, dbName),
23
+ logging,
24
+ pool: {
25
+ max: 5,
26
+ min: 0,
27
+ acquire: 30000,
28
+ idle: 10000
29
+ }
30
+ });
31
+ Namespaces.init(
32
+ {
33
+ purl: {
34
+ type: DataTypes.STRING,
35
+ allowNull: false,
36
+ primaryKey: true
37
+ },
38
+ data: {
39
+ type: DataTypes.JSON,
40
+ allowNull: false
41
+ }
42
+ },
43
+ { sequelize, modelName: "Namespaces" }
44
+ );
45
+ Usages.init(
46
+ {
47
+ purl: {
48
+ type: DataTypes.STRING,
49
+ allowNull: false,
50
+ primaryKey: true
51
+ },
52
+ data: {
53
+ type: DataTypes.JSON,
54
+ allowNull: false
55
+ }
56
+ },
57
+ { sequelize, modelName: "Usages" }
58
+ );
59
+ DataFlows.init(
60
+ {
61
+ purl: {
62
+ type: DataTypes.STRING,
63
+ allowNull: false,
64
+ primaryKey: true
65
+ },
66
+ data: {
67
+ type: DataTypes.JSON,
68
+ allowNull: false
69
+ }
70
+ },
71
+ { sequelize, modelName: "DataFlows" }
72
+ );
73
+ await sequelize.sync();
74
+ return {
75
+ sequelize,
76
+ Namespaces,
77
+ Usages,
78
+ DataFlows
79
+ };
80
+ };