@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 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
@@ -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
  ![SBoM signing](sbom-sign.jpg)
387
394
 
388
- ### Verifying the signature (Node.js example)
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 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.
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 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
- import { basename, resolve, isAbsolute } from "path";
5
+ import { basename, resolve, isAbsolute, relative } from "path";
6
6
 
7
- const IGNORE_DIRS = [
8
- "node_modules",
9
- "venv",
10
- "docs",
11
- "test",
12
- "tests",
13
- "e2e",
14
- "examples",
15
- "cypress",
16
- "site-packages",
17
- "typings",
18
- "api_docs",
19
- "dev_docs",
20
- "types",
21
- "mock",
22
- "mocks"
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
- "(conf|test|spec|mock|\\.d)\\.(js|ts|tsx)$",
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, pathway) => {
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 module = pathway;
122
+ let moduleFullPath = pathway;
123
+ let wasAbsolute = false;
97
124
  if (/\.\//g.test(pathway) || /\.\.\//g.test(pathway)) {
98
- module = resolve(file, "..", pathway);
99
- }
100
-
101
- // initialise or increase reference count for file
102
- if (Object.prototype.hasOwnProperty.call(allImports, module)) {
103
- allImports[module] = allImports[module] + 1;
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 (!isAbsolute(module) && module.includes("/")) {
111
- const modPkg = module.split("/")[0];
112
- if (Object.prototype.hasOwnProperty.call(allImports, modPkg)) {
113
- allImports[modPkg] = allImports[modPkg] + 1;
114
- } else {
115
- allImports[modPkg] = 1;
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(readFileSync(file, "utf-8"), babelParserOptions);
126
- babelTraverse(ast, {
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(allImports, file, path.node.source.value);
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].value);
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].value);
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.value);
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(allImports, file, path.node.source.value);
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: jsonPayload,
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
+ })();