@cyclonedx/cdxgen 9.4.0 → 9.6.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 +51 -79
- package/analyzer.js +118 -51
- package/bin/cdxgen.js +19 -2
- package/bin/evinse.js +123 -0
- package/bin/repl.js +123 -7
- package/bin/verify.js +61 -0
- package/db.js +80 -0
- package/display.js +113 -1
- package/docker.test.js +9 -0
- package/evinser.js +1038 -0
- package/evinser.test.js +93 -0
- package/index.js +241 -115
- package/package.json +10 -5
- package/utils.js +561 -163
- package/utils.test.js +98 -16
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
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
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
|
|
@@ -282,39 +282,42 @@ cdxgen can retain the dependency tree under the `dependencies` attribute for a s
|
|
|
282
282
|
- pnpm-lock.yaml
|
|
283
283
|
- Maven (pom.xml)
|
|
284
284
|
- Gradle
|
|
285
|
+
- Scala SBT
|
|
285
286
|
- Python (requirements.txt, setup.py, pyproject.toml, poetry.lock)
|
|
286
287
|
|
|
287
288
|
## Environment variables
|
|
288
289
|
|
|
289
|
-
| Variable | Description
|
|
290
|
-
| ---------------------------- |
|
|
291
|
-
| CDXGEN_DEBUG_MODE | Set to `debug` to enable debug messages
|
|
292
|
-
| GITHUB_TOKEN | Specify GitHub token to prevent traffic shaping while querying license and repo information
|
|
293
|
-
| MVN_CMD | Set to override maven command
|
|
294
|
-
| MVN_ARGS | Set to pass additional arguments such as profile or settings to maven
|
|
295
|
-
| MAVEN_HOME | Specify maven home
|
|
296
|
-
| GRADLE_CACHE_DIR | Specify gradle cache directory. Useful for class name resolving
|
|
297
|
-
| GRADLE_MULTI_PROJECT_MODE | Unused. Automatically handled
|
|
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
|
|
301
|
-
| GRADLE_HOME | Specify gradle home
|
|
302
|
-
| GRADLE_CMD | Set to override gradle command
|
|
303
|
-
| GRADLE_DEPENDENCY_TASK | By default cdxgen use the task "dependencies" to collect packages. Set to override the task name.
|
|
304
|
-
| SBT_CACHE_DIR | Specify sbt cache directory. Useful for class name resolving
|
|
305
|
-
| FETCH_LICENSE | Set this variable to `true` or `1` to fetch license information from the registry. npm and golang
|
|
306
|
-
| USE_GOSUM | Set to `true` or `1` to generate BOMs for golang projects using go.sum as the dependency source of truth, instead of go.mod
|
|
307
|
-
| CDXGEN_TIMEOUT_MS | Default timeout for known execution involving maven, gradle or sbt
|
|
308
|
-
| CDXGEN_SERVER_TIMEOUT_MS | Default timeout in server mode
|
|
309
|
-
| BAZEL_TARGET | Bazel target to build. Default :all (Eg: //java-maven)
|
|
310
|
-
| CLJ_CMD | Set to override the clojure cli command
|
|
311
|
-
| LEIN_CMD | Set to override the leiningen command
|
|
312
|
-
| SBOM_SIGN_ALGORITHM | Signature algorithm. Some valid values are RS256, RS384, RS512, PS256, PS384, PS512, ES256 etc
|
|
313
|
-
| SBOM_SIGN_PRIVATE_KEY | Private key to use for signing
|
|
314
|
-
| SBOM_SIGN_PUBLIC_KEY | Optional. Public key to include in the SBoM signature
|
|
315
|
-
| CDX_MAVEN_PLUGIN | CycloneDX Maven plugin to use. Default "org.cyclonedx:cyclonedx-maven-plugin:2.7.8"
|
|
316
|
-
| CDX_MAVEN_GOAL | CycloneDX Maven plugin goal to use. Default makeAggregateBom. Other options: makeBom, makePackageBom
|
|
317
|
-
| CDX_MAVEN_INCLUDE_TEST_SCOPE | Whether test scoped dependencies should be included from Maven projects, Default: true
|
|
290
|
+
| Variable | Description |
|
|
291
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
292
|
+
| CDXGEN_DEBUG_MODE | Set to `debug` to enable debug messages |
|
|
293
|
+
| GITHUB_TOKEN | Specify GitHub token to prevent traffic shaping while querying license and repo information |
|
|
294
|
+
| MVN_CMD | Set to override maven command |
|
|
295
|
+
| MVN_ARGS | Set to pass additional arguments such as profile or settings to maven |
|
|
296
|
+
| MAVEN_HOME | Specify maven home |
|
|
297
|
+
| GRADLE_CACHE_DIR | Specify gradle cache directory. Useful for class name resolving |
|
|
298
|
+
| GRADLE_MULTI_PROJECT_MODE | Unused. Automatically handled |
|
|
299
|
+
| GRADLE_ARGS | Set to pass additional arguments such as profile or settings to gradle (all tasks). Eg: --configuration runtimeClassPath |
|
|
300
|
+
| GRADLE_ARGS_PROPERTIES | Set to pass additional arguments only to the `gradle properties` task, used for collecting metadata about the project |
|
|
301
|
+
| GRADLE_ARGS_DEPENDENCIES | Set to pass additional arguments only to the `gradle dependencies` task, used for listing actual project dependencies |
|
|
302
|
+
| GRADLE_HOME | Specify gradle home |
|
|
303
|
+
| GRADLE_CMD | Set to override gradle command |
|
|
304
|
+
| GRADLE_DEPENDENCY_TASK | By default cdxgen use the task "dependencies" to collect packages. Set to override the task name. |
|
|
305
|
+
| SBT_CACHE_DIR | Specify sbt cache directory. Useful for class name resolving |
|
|
306
|
+
| FETCH_LICENSE | Set this variable to `true` or `1` to fetch license information from the registry. npm and golang |
|
|
307
|
+
| USE_GOSUM | Set to `true` or `1` to generate BOMs for golang projects using go.sum as the dependency source of truth, instead of go.mod |
|
|
308
|
+
| CDXGEN_TIMEOUT_MS | Default timeout for known execution involving maven, gradle or sbt |
|
|
309
|
+
| CDXGEN_SERVER_TIMEOUT_MS | Default timeout in server mode |
|
|
310
|
+
| BAZEL_TARGET | Bazel target to build. Default :all (Eg: //java-maven) |
|
|
311
|
+
| CLJ_CMD | Set to override the clojure cli command |
|
|
312
|
+
| LEIN_CMD | Set to override the leiningen command |
|
|
313
|
+
| SBOM_SIGN_ALGORITHM | Signature algorithm. Some valid values are RS256, RS384, RS512, PS256, PS384, PS512, ES256 etc |
|
|
314
|
+
| SBOM_SIGN_PRIVATE_KEY | Private key to use for signing |
|
|
315
|
+
| SBOM_SIGN_PUBLIC_KEY | Optional. Public key to include in the SBoM signature |
|
|
316
|
+
| CDX_MAVEN_PLUGIN | CycloneDX Maven plugin to use. Default "org.cyclonedx:cyclonedx-maven-plugin:2.7.8" |
|
|
317
|
+
| CDX_MAVEN_GOAL | CycloneDX Maven plugin goal to use. Default makeAggregateBom. Other options: makeBom, makePackageBom |
|
|
318
|
+
| CDX_MAVEN_INCLUDE_TEST_SCOPE | Whether test scoped dependencies should be included from Maven projects, Default: true |
|
|
319
|
+
| ASTGEN_IGNORE_DIRS | Comma separated list of directories to ignore while analyzing using babel. The environment variable is also used by atom and astgen. |
|
|
320
|
+
| ASTGEN_IGNORE_FILE_PATTERN | Ignore regex to use |
|
|
318
321
|
|
|
319
322
|
## Plugins
|
|
320
323
|
|
|
@@ -373,6 +376,10 @@ cdxgen -t os
|
|
|
373
376
|
|
|
374
377
|
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
378
|
|
|
379
|
+
## Generating component evidence
|
|
380
|
+
|
|
381
|
+
See [evinse mode](./ADVANCED.md) in the advanced documentation.
|
|
382
|
+
|
|
376
383
|
## SBoM signing
|
|
377
384
|
|
|
378
385
|
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 +392,16 @@ To generate test public/private key pairs, you can run cdxgen by passing the arg
|
|
|
385
392
|
|
|
386
393
|

|
|
387
394
|
|
|
388
|
-
### Verifying the signature
|
|
395
|
+
### Verifying the signature
|
|
396
|
+
|
|
397
|
+
Use the bundled `cdx-verify` command which supports verifying a single signature added at the bom level.
|
|
398
|
+
|
|
399
|
+
```shell
|
|
400
|
+
npm install -g @cyclonedx/cdxgen
|
|
401
|
+
cdx-verify -i bom.json --public-key public.key
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Custom verification tool (Node.js example)
|
|
389
405
|
|
|
390
406
|
There are many [libraries](https://jwt.io/#libraries-io) available to validate JSON Web Tokens. Below is a javascript example.
|
|
391
407
|
|
|
@@ -410,7 +426,7 @@ if (validationResult) {
|
|
|
410
426
|
|
|
411
427
|
## Automatic services detection
|
|
412
428
|
|
|
413
|
-
cdxgen
|
|
429
|
+
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
430
|
|
|
415
431
|
## Conversion to SPDX format
|
|
416
432
|
|
|
@@ -443,50 +459,6 @@ const bomNSData = await createBom(filePath, options);
|
|
|
443
459
|
const dbody = await submitBom(args, bomNSData.bomJson);
|
|
444
460
|
```
|
|
445
461
|
|
|
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
462
|
## Node.js >= 20 permission model
|
|
491
463
|
|
|
492
464
|
Refer to the [permissions document](./docs/PERMISSIONS.md)
|
package/analyzer.js
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
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
|
-
import { basename, resolve, isAbsolute } from "path";
|
|
5
|
+
import { basename, resolve, isAbsolute, relative } from "path";
|
|
6
6
|
|
|
7
|
-
const IGNORE_DIRS =
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
const IGNORE_DIRS = process.env.ASTGEN_IGNORE_DIRS
|
|
8
|
+
? process.env.ASTGEN_IGNORE_DIRS.split(",")
|
|
9
|
+
: [
|
|
10
|
+
"node_modules",
|
|
11
|
+
"venv",
|
|
12
|
+
"docs",
|
|
13
|
+
"test",
|
|
14
|
+
"tests",
|
|
15
|
+
"e2e",
|
|
16
|
+
"examples",
|
|
17
|
+
"cypress",
|
|
18
|
+
"site-packages",
|
|
19
|
+
"typings",
|
|
20
|
+
"api_docs",
|
|
21
|
+
"dev_docs",
|
|
22
|
+
"types",
|
|
23
|
+
"mock",
|
|
24
|
+
"mocks",
|
|
25
|
+
"jest-cache",
|
|
26
|
+
"eslint-rules",
|
|
27
|
+
"codemods",
|
|
28
|
+
"flow-typed",
|
|
29
|
+
"i18n",
|
|
30
|
+
"__tests__"
|
|
31
|
+
];
|
|
24
32
|
|
|
25
33
|
const IGNORE_FILE_PATTERN = new RegExp(
|
|
26
|
-
|
|
34
|
+
process.env.ASTGEN_IGNORE_FILE_PATTERN ||
|
|
35
|
+
"(conf|config|test|spec|mock|\\.d)\\.(js|ts|tsx)$",
|
|
27
36
|
"i"
|
|
28
37
|
);
|
|
29
38
|
|
|
@@ -33,7 +42,7 @@ const getAllFiles = (dir, extn, files, result, regex) => {
|
|
|
33
42
|
regex = regex || new RegExp(`\\${extn}$`);
|
|
34
43
|
|
|
35
44
|
for (let i = 0; i < files.length; i++) {
|
|
36
|
-
if (IGNORE_FILE_PATTERN.test(files[i])) {
|
|
45
|
+
if (IGNORE_FILE_PATTERN.test(files[i]) || files[i].startsWith(".")) {
|
|
37
46
|
continue;
|
|
38
47
|
}
|
|
39
48
|
const file = join(dir, files[i]);
|
|
@@ -64,6 +73,7 @@ const babelParserOptions = {
|
|
|
64
73
|
sourceType: "unambiguous",
|
|
65
74
|
allowImportExportEverywhere: true,
|
|
66
75
|
allowAwaitOutsideFunction: true,
|
|
76
|
+
allowNewTargetOutsideFunction: true,
|
|
67
77
|
allowReturnOutsideFunction: true,
|
|
68
78
|
allowSuperOutsideMethod: true,
|
|
69
79
|
errorRecovery: true,
|
|
@@ -86,48 +96,95 @@ const babelParserOptions = {
|
|
|
86
96
|
* Filter only references to (t|jsx?) or (less|scss) files for now.
|
|
87
97
|
* Opt to use our relative paths.
|
|
88
98
|
*/
|
|
89
|
-
const setFileRef = (allImports, file,
|
|
99
|
+
const setFileRef = (allImports, src, file, pathnode, specifiers = []) => {
|
|
100
|
+
const pathway = pathnode.value || pathnode.name;
|
|
101
|
+
const sourceLoc = pathnode.loc?.start;
|
|
102
|
+
if (!pathway) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const fileRelativeLoc = relative(src, file);
|
|
90
106
|
// remove unexpected extension imports
|
|
91
107
|
if (/\.(svg|png|jpg|d\.ts)/.test(pathway)) {
|
|
92
108
|
return;
|
|
93
109
|
}
|
|
94
|
-
|
|
110
|
+
const importedModules = specifiers
|
|
111
|
+
.map((s) => s.imported?.name)
|
|
112
|
+
.filter((v) => v !== undefined);
|
|
113
|
+
const occurrence = {
|
|
114
|
+
importedAs: pathway,
|
|
115
|
+
importedModules,
|
|
116
|
+
isExternal: true,
|
|
117
|
+
fileName: fileRelativeLoc,
|
|
118
|
+
lineNumber: sourceLoc && sourceLoc.line ? sourceLoc.line : undefined,
|
|
119
|
+
columnNumber: sourceLoc && sourceLoc.column ? sourceLoc.column : undefined
|
|
120
|
+
};
|
|
95
121
|
// replace relative imports with full path
|
|
96
|
-
let
|
|
122
|
+
let moduleFullPath = pathway;
|
|
123
|
+
let wasAbsolute = false;
|
|
97
124
|
if (/\.\//g.test(pathway) || /\.\.\//g.test(pathway)) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} else {
|
|
105
|
-
allImports[module] = 1;
|
|
125
|
+
moduleFullPath = resolve(file, "..", pathway);
|
|
126
|
+
if (isAbsolute(moduleFullPath)) {
|
|
127
|
+
moduleFullPath = relative(src, moduleFullPath);
|
|
128
|
+
wasAbsolute = true;
|
|
129
|
+
}
|
|
130
|
+
occurrence.isExternal = false;
|
|
106
131
|
}
|
|
107
|
-
|
|
132
|
+
allImports[moduleFullPath] = allImports[moduleFullPath] || new Set();
|
|
133
|
+
allImports[moduleFullPath].add(occurrence);
|
|
108
134
|
// Handle module package name
|
|
109
135
|
// Eg: zone.js/dist/zone will be referred to as zone.js in package.json
|
|
110
|
-
if (!
|
|
111
|
-
const modPkg =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
if (!wasAbsolute && moduleFullPath.includes("/")) {
|
|
137
|
+
const modPkg = moduleFullPath.split("/")[0];
|
|
138
|
+
allImports[modPkg] = allImports[modPkg] || new Set();
|
|
139
|
+
allImports[modPkg].add(occurrence);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const vueCleaningRegex = /<\/*script.*>|<style[\s\S]*style>|<\/*br>/gi;
|
|
144
|
+
const vueTemplateRegex = /(<template.*>)([\s\S]*)(<\/template>)/gi;
|
|
145
|
+
const vueCommentRegex = /<!--[\s\S]*?-->/gi;
|
|
146
|
+
const vueBindRegex = /(:\[)([\s\S]*?)(\])/gi;
|
|
147
|
+
const vuePropRegex = /\s([.:@])([a-zA-Z]*?=)/gi;
|
|
148
|
+
|
|
149
|
+
const fileToParseableCode = (file) => {
|
|
150
|
+
let code = readFileSync(file, "utf-8");
|
|
151
|
+
if (file.endsWith(".vue") || file.endsWith(".svelte")) {
|
|
152
|
+
code = code
|
|
153
|
+
.replace(vueCommentRegex, function (match) {
|
|
154
|
+
return match.replaceAll(/\S/g, " ");
|
|
155
|
+
})
|
|
156
|
+
.replace(vueCleaningRegex, function (match) {
|
|
157
|
+
return match.replaceAll(/\S/g, " ").substring(1) + ";";
|
|
158
|
+
})
|
|
159
|
+
.replace(vueBindRegex, function (match, grA, grB, grC) {
|
|
160
|
+
return grA.replaceAll(/\S/g, " ") + grB + grC.replaceAll(/\S/g, " ");
|
|
161
|
+
})
|
|
162
|
+
.replace(vuePropRegex, function (match, grA, grB) {
|
|
163
|
+
return " " + grA.replace(/[.:@]/g, " ") + grB;
|
|
164
|
+
})
|
|
165
|
+
.replace(vueTemplateRegex, function (match, grA, grB, grC) {
|
|
166
|
+
return grA + grB.replaceAll("{{", "{ ").replaceAll("}}", " }") + grC;
|
|
167
|
+
});
|
|
117
168
|
}
|
|
169
|
+
return code;
|
|
118
170
|
};
|
|
119
171
|
|
|
120
172
|
/**
|
|
121
173
|
* Check AST tree for any (j|tsx?) files and set a file
|
|
122
174
|
* references for any import, require or dynamic import files.
|
|
123
175
|
*/
|
|
124
|
-
const parseFileASTTree = (file, allImports) => {
|
|
125
|
-
const ast = parse(
|
|
126
|
-
|
|
127
|
-
// Used for all ES6 import statements
|
|
176
|
+
const parseFileASTTree = (src, file, allImports) => {
|
|
177
|
+
const ast = parse(fileToParseableCode(file), babelParserOptions);
|
|
178
|
+
traverse.default(ast, {
|
|
128
179
|
ImportDeclaration: (path) => {
|
|
129
180
|
if (path && path.node) {
|
|
130
|
-
setFileRef(
|
|
181
|
+
setFileRef(
|
|
182
|
+
allImports,
|
|
183
|
+
src,
|
|
184
|
+
file,
|
|
185
|
+
path.node.source,
|
|
186
|
+
path.node.specifiers
|
|
187
|
+
);
|
|
131
188
|
}
|
|
132
189
|
},
|
|
133
190
|
// For require('') statements
|
|
@@ -138,23 +195,29 @@ const parseFileASTTree = (file, allImports) => {
|
|
|
138
195
|
path.node.name === "require" &&
|
|
139
196
|
path.parent.type === "CallExpression"
|
|
140
197
|
) {
|
|
141
|
-
setFileRef(allImports, file, path.parent.arguments[0]
|
|
198
|
+
setFileRef(allImports, src, file, path.parent.arguments[0]);
|
|
142
199
|
}
|
|
143
200
|
},
|
|
144
201
|
// Use for dynamic imports like routes.jsx
|
|
145
202
|
CallExpression: (path) => {
|
|
146
203
|
if (path && path.node && path.node.callee.type === "Import") {
|
|
147
|
-
setFileRef(allImports, file, path.node.arguments[0]
|
|
204
|
+
setFileRef(allImports, src, file, path.node.arguments[0]);
|
|
148
205
|
}
|
|
149
206
|
},
|
|
150
207
|
// Use for export barrells
|
|
151
208
|
ExportAllDeclaration: (path) => {
|
|
152
|
-
setFileRef(allImports, file, path.node.source
|
|
209
|
+
setFileRef(allImports, src, file, path.node.source);
|
|
153
210
|
},
|
|
154
211
|
ExportNamedDeclaration: (path) => {
|
|
155
212
|
// ensure there is a path export
|
|
156
213
|
if (path && path.node && path.node.source) {
|
|
157
|
-
setFileRef(
|
|
214
|
+
setFileRef(
|
|
215
|
+
allImports,
|
|
216
|
+
src,
|
|
217
|
+
file,
|
|
218
|
+
path.node.source,
|
|
219
|
+
path.node.specifiers
|
|
220
|
+
);
|
|
158
221
|
}
|
|
159
222
|
}
|
|
160
223
|
});
|
|
@@ -167,8 +230,12 @@ const getAllSrcJSAndTSFiles = (src) =>
|
|
|
167
230
|
Promise.all([
|
|
168
231
|
getAllFiles(src, ".js"),
|
|
169
232
|
getAllFiles(src, ".jsx"),
|
|
233
|
+
getAllFiles(src, ".cjs"),
|
|
234
|
+
getAllFiles(src, ".mjs"),
|
|
170
235
|
getAllFiles(src, ".ts"),
|
|
171
|
-
getAllFiles(src, ".tsx")
|
|
236
|
+
getAllFiles(src, ".tsx"),
|
|
237
|
+
getAllFiles(src, ".vue"),
|
|
238
|
+
getAllFiles(src, ".svelte")
|
|
172
239
|
]);
|
|
173
240
|
|
|
174
241
|
/**
|
|
@@ -182,7 +249,7 @@ export const findJSImports = async (src) => {
|
|
|
182
249
|
const srcFiles = promiseMap.flatMap((d) => d);
|
|
183
250
|
for (const file of srcFiles) {
|
|
184
251
|
try {
|
|
185
|
-
parseFileASTTree(file, allImports);
|
|
252
|
+
parseFileASTTree(src, file, allImports);
|
|
186
253
|
} catch (err) {
|
|
187
254
|
errFiles.push(file);
|
|
188
255
|
}
|
package/bin/cdxgen.js
CHANGED
|
@@ -313,13 +313,30 @@ const checkPermissions = (filePath) => {
|
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
315
|
try {
|
|
316
|
+
// Sign the individual components
|
|
317
|
+
// Let's leave the services unsigned for now since it might require additional cleansing
|
|
318
|
+
const bomJsonUnsignedObj = JSON.parse(jsonPayload);
|
|
319
|
+
for (const comp of bomJsonUnsignedObj.components) {
|
|
320
|
+
const compSignature = jws.sign({
|
|
321
|
+
header: { alg },
|
|
322
|
+
payload: comp,
|
|
323
|
+
privateKey: privateKeyToUse
|
|
324
|
+
});
|
|
325
|
+
const compSignatureBlock = {
|
|
326
|
+
algorithm: alg,
|
|
327
|
+
value: compSignature
|
|
328
|
+
};
|
|
329
|
+
if (jwkPublicKey) {
|
|
330
|
+
compSignatureBlock.publicKey = jwkPublicKey;
|
|
331
|
+
}
|
|
332
|
+
comp.signature = compSignatureBlock;
|
|
333
|
+
}
|
|
316
334
|
const signature = jws.sign({
|
|
317
335
|
header: { alg },
|
|
318
|
-
payload:
|
|
336
|
+
payload: JSON.stringify(bomJsonUnsignedObj, null, 2),
|
|
319
337
|
privateKey: privateKeyToUse
|
|
320
338
|
});
|
|
321
339
|
if (signature) {
|
|
322
|
-
const bomJsonUnsignedObj = JSON.parse(jsonPayload);
|
|
323
340
|
const signatureBlock = {
|
|
324
341
|
algorithm: alg,
|
|
325
342
|
value: signature
|
package/bin/evinse.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Evinse (Evinse Verification Is Nearly SBoM Evidence)
|
|
4
|
+
import yargs from "yargs";
|
|
5
|
+
import { hideBin } from "yargs/helpers";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { homedir, platform as _platform } from "node:os";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
import { analyzeProject, createEvinseFile, prepareDB } from "../evinser.js";
|
|
11
|
+
import { validateBom } from "../validator.js";
|
|
12
|
+
import { printCallStack, printOccurrences, printServices } from "../display.js";
|
|
13
|
+
|
|
14
|
+
const isWin = _platform() === "win32";
|
|
15
|
+
const isMac = _platform() === "darwin";
|
|
16
|
+
let ATOM_DB = join(homedir(), ".local", "share", ".atomdb");
|
|
17
|
+
if (isWin) {
|
|
18
|
+
ATOM_DB = join(homedir(), "AppData", "Local", ".atomdb");
|
|
19
|
+
} else if (isMac) {
|
|
20
|
+
ATOM_DB = join(homedir(), "Library", "Application Support", ".atomdb");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!process.env.ATOM_DB && !fs.existsSync(ATOM_DB)) {
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(ATOM_DB, { recursive: true });
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const args = yargs(hideBin(process.argv))
|
|
31
|
+
.option("input", {
|
|
32
|
+
alias: "i",
|
|
33
|
+
description: "Input SBoM file. Default bom.json",
|
|
34
|
+
default: "bom.json"
|
|
35
|
+
})
|
|
36
|
+
.option("output", {
|
|
37
|
+
alias: "o",
|
|
38
|
+
description: "Output file. Default bom.evinse.json",
|
|
39
|
+
default: "bom.evinse.json"
|
|
40
|
+
})
|
|
41
|
+
.option("language", {
|
|
42
|
+
alias: "l",
|
|
43
|
+
description: "Application language",
|
|
44
|
+
default: "java",
|
|
45
|
+
choices: ["java", "jar", "javascript", "python", "android", "cpp"]
|
|
46
|
+
})
|
|
47
|
+
.option("db-path", {
|
|
48
|
+
description: `Atom slices DB path. Default ${ATOM_DB}`,
|
|
49
|
+
default: process.env.ATOM_DB || ATOM_DB
|
|
50
|
+
})
|
|
51
|
+
.option("force", {
|
|
52
|
+
description: "Force creation of the database",
|
|
53
|
+
default: false,
|
|
54
|
+
type: "boolean"
|
|
55
|
+
})
|
|
56
|
+
.option("skip-maven-collector", {
|
|
57
|
+
description:
|
|
58
|
+
"Skip collecting jars from maven and gradle caches. Can speedup re-runs if the data was cached previously.",
|
|
59
|
+
default: false,
|
|
60
|
+
type: "boolean"
|
|
61
|
+
})
|
|
62
|
+
.option("with-deep-jar-collector", {
|
|
63
|
+
description:
|
|
64
|
+
"Enable collection of all jars from maven cache directory. Useful to improve the recall for callstack evidence.",
|
|
65
|
+
default: false,
|
|
66
|
+
type: "boolean"
|
|
67
|
+
})
|
|
68
|
+
.option("annotate", {
|
|
69
|
+
description: "Include contents of atom slices as annotations",
|
|
70
|
+
default: false,
|
|
71
|
+
type: "boolean"
|
|
72
|
+
})
|
|
73
|
+
.option("with-data-flow", {
|
|
74
|
+
description: "Enable inter-procedural data-flow slicing.",
|
|
75
|
+
default: false,
|
|
76
|
+
type: "boolean"
|
|
77
|
+
})
|
|
78
|
+
.option("usages-slices-file", {
|
|
79
|
+
description: "Use an existing usages slices file.",
|
|
80
|
+
default: "usages.slices.json"
|
|
81
|
+
})
|
|
82
|
+
.option("data-flow-slices-file", {
|
|
83
|
+
description: "Use an existing data-flow slices file.",
|
|
84
|
+
default: "data-flow.slices.json"
|
|
85
|
+
})
|
|
86
|
+
.option("print", {
|
|
87
|
+
alias: "p",
|
|
88
|
+
type: "boolean",
|
|
89
|
+
description: "Print the evidences as table"
|
|
90
|
+
})
|
|
91
|
+
.scriptName("evinse")
|
|
92
|
+
.version()
|
|
93
|
+
.help("h").argv;
|
|
94
|
+
|
|
95
|
+
const evinseArt = `
|
|
96
|
+
███████╗██╗ ██╗██╗███╗ ██╗███████╗███████╗
|
|
97
|
+
██╔════╝██║ ██║██║████╗ ██║██╔════╝██╔════╝
|
|
98
|
+
█████╗ ██║ ██║██║██╔██╗ ██║███████╗█████╗
|
|
99
|
+
██╔══╝ ╚██╗ ██╔╝██║██║╚██╗██║╚════██║██╔══╝
|
|
100
|
+
███████╗ ╚████╔╝ ██║██║ ╚████║███████║███████╗
|
|
101
|
+
╚══════╝ ╚═══╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
console.log(evinseArt);
|
|
105
|
+
(async () => {
|
|
106
|
+
// First, prepare the database by cataloging jars and other libraries
|
|
107
|
+
const dbObjMap = await prepareDB(args);
|
|
108
|
+
if (dbObjMap) {
|
|
109
|
+
// Analyze the project using atom. Convert package namespaces to purl using the db
|
|
110
|
+
const sliceArtefacts = await analyzeProject(dbObjMap, args);
|
|
111
|
+
// Create the SBoM with Evidence
|
|
112
|
+
const bomJson = createEvinseFile(sliceArtefacts, args);
|
|
113
|
+
// Validate our final SBoM
|
|
114
|
+
if (!validateBom(bomJson)) {
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (args.print) {
|
|
118
|
+
printOccurrences(bomJson);
|
|
119
|
+
printCallStack(bomJson);
|
|
120
|
+
printServices(bomJson);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})();
|