@cyclonedx/cdxgen 9.3.2 → 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,19 @@
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
+
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">
12
18
 
13
19
  ## Supported languages and package format
14
20
 
@@ -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
 
@@ -87,7 +93,7 @@ sudo npm install -g @cyclonedx/cdxgen@8.6.0
87
93
  Deno install is also supported.
88
94
 
89
95
  ```shell
90
- 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"
91
97
  ```
92
98
 
93
99
  You can also use the cdxgen container image
@@ -367,6 +373,10 @@ cdxgen -t os
367
373
 
368
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.
369
375
 
376
+ ## Generating component evidence
377
+
378
+ See [evinse mode](./ADVANCED.md) in the advanced documentation.
379
+
370
380
  ## SBoM signing
371
381
 
372
382
  cdxgen can sign the generated SBoM json file to increase authenticity and non-repudiation capabilities. To enable this, set the following environment variables.
@@ -375,13 +385,45 @@ cdxgen can sign the generated SBoM json file to increase authenticity and non-re
375
385
  - SBOM_SIGN_PRIVATE_KEY: Location to the RSA private key
376
386
  - SBOM_SIGN_PUBLIC_KEY: Optional. Location to the RSA public key
377
387
 
378
- 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.
388
+ 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.
379
389
 
380
390
  ![SBoM signing](sbom-sign.jpg)
381
391
 
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)
402
+
403
+ There are many [libraries](https://jwt.io/#libraries-io) available to validate JSON Web Tokens. Below is a javascript example.
404
+
405
+ ```js
406
+ # npm install jws
407
+ const jws = require("jws");
408
+ const fs = require("fs");
409
+ // Location of the SBoM json file
410
+ const bomJsonFile = "bom.json";
411
+ // Location of the public key
412
+ const publicKeyFile = "public.key";
413
+ const bomJson = JSON.parse(fs.readFileSync(bomJsonFile, "utf8"));
414
+ // Retrieve the signature
415
+ const bomSignature = bomJson.signature.value;
416
+ const validationResult = jws.verify(bomSignature, bomJson.signature.algorithm, fs.readFileSync(publicKeyFile, "utf8"));
417
+ if (validationResult) {
418
+ console.log("Signature is valid!");
419
+ } else {
420
+ console.log("SBoM signature is invalid :(");
421
+ }
422
+ ```
423
+
382
424
  ## Automatic services detection
383
425
 
384
- 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.
385
427
 
386
428
  ## Conversion to SPDX format
387
429
 
@@ -427,3 +469,7 @@ npm run lint
427
469
  npm run pretty
428
470
  npm test
429
471
  ```
472
+
473
+ ## Enterprise support
474
+
475
+ 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/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/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/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
+ })();