@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 +53 -7
- package/analyzer.js +29 -4
- package/bin/cdxgen.js +22 -1
- package/bin/evinse.js +121 -0
- package/bin/repl.js +425 -0
- package/bin/verify.js +39 -0
- package/db.js +80 -0
- package/display.js +103 -1
- package/docker.js +1 -1
- package/docker.test.js +17 -5
- package/evinser.js +827 -0
- package/evinser.test.js +35 -0
- package/index.js +151 -72
- package/package.json +19 -11
- package/piptree.js +2 -2
- package/utils.js +447 -190
- package/utils.test.js +88 -18
package/README.md
CHANGED
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
cdxgen is a cli tool and
|
|
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
|
|
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
|
-
|
|
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
|

|
|
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
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
+
})();
|