@cyclonedx/cdxgen 8.5.3 → 9.0.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 +55 -45
- package/analyzer.js +15 -19
- package/bin/{cdxgen → cdxgen.js} +76 -25
- package/binary.js +52 -52
- package/data/known-licenses.json +31 -0
- package/{lic-mapping.json → data/lic-mapping.json} +11 -39
- package/data/pypi-pkg-aliases.json +1165 -0
- package/data/python-stdlib.json +308 -0
- package/{queries.json → data/queries.json} +8 -8
- package/data/vendor-alias.json +10 -0
- package/docker.js +157 -127
- package/docker.test.js +18 -14
- package/index.js +529 -417
- package/jest.config.js +6 -180
- package/package.json +20 -22
- package/server.js +12 -12
- package/utils.js +793 -377
- package/utils.test.js +395 -301
- package/.eslintrc.js +0 -15
- package/known-licenses.json +0 -27
- package/vendor-alias.json +0 -10
- /package/{spdx-licenses.json → data/spdx-licenses.json} +0 -0
package/README.md
CHANGED
|
@@ -8,40 +8,39 @@ When used with plugins, cdxgen could generate an SBoM for Linux docker images an
|
|
|
8
8
|
|
|
9
9
|
## Supported languages and package format
|
|
10
10
|
|
|
11
|
-
| Language/Platform
|
|
12
|
-
|
|
|
13
|
-
| node.js
|
|
14
|
-
| java
|
|
15
|
-
| php
|
|
16
|
-
| python
|
|
17
|
-
| go
|
|
18
|
-
| ruby
|
|
19
|
-
| rust
|
|
20
|
-
| .Net
|
|
21
|
-
| dart
|
|
22
|
-
| haskell
|
|
23
|
-
| elixir
|
|
24
|
-
| c/c++
|
|
25
|
-
| clojure
|
|
26
|
-
| swift
|
|
27
|
-
| docker / oci image
|
|
28
|
-
| GitHub Actions
|
|
29
|
-
| Linux
|
|
30
|
-
| Windows
|
|
31
|
-
| Jenkins Plugins
|
|
32
|
-
| Helm Charts
|
|
33
|
-
| Skaffold
|
|
34
|
-
| kustomization
|
|
35
|
-
| Tekton tasks
|
|
36
|
-
| Kubernetes
|
|
37
|
-
| Maven Cache
|
|
38
|
-
| SBT Cache
|
|
39
|
-
| Gradle Cache
|
|
40
|
-
| Helm Index
|
|
41
|
-
| Docker compose
|
|
42
|
-
| Google CloudBuild configuration
|
|
43
|
-
| OpenAPI
|
|
44
|
-
| [Privado](https://www.privado.ai?utm_source=cyclonedx) | privado.json | Data and service information will be included. Use with universal mode. |
|
|
11
|
+
| Language/Platform | Package format | Transitive dependencies |
|
|
12
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| node.js | npm-shrinkwrap.json, package-lock.json, pnpm-lock.yaml, yarn.lock, rush.js, bower.json, .min.js | Yes except .min.js |
|
|
14
|
+
| java | maven (pom.xml [1]), gradle (build.gradle, .kts), scala (sbt), bazel | Yes unless pom.xml is manually parsed due to unavailability of maven or errors |
|
|
15
|
+
| php | composer.lock | Yes |
|
|
16
|
+
| python | pyproject.toml, setup.py, requirements.txt [2], Pipfile.lock, poetry.lock, bdist_wheel, .whl, .egg-info | Yes using the automatic pip install/freeze. When disabled, only with Pipfile.lock and poetry.lock |
|
|
17
|
+
| go | binary, go.mod, go.sum, Gopkg.lock | Yes except binary |
|
|
18
|
+
| ruby | Gemfile.lock, gemspec | Only for Gemfile.lock |
|
|
19
|
+
| rust | binary, Cargo.toml, Cargo.lock | Only for Cargo.lock |
|
|
20
|
+
| .Net | .csproj, packages.config, project.assets.json [3], packages.lock.json, .nupkg | Only for project.assets.json, packages.lock.json |
|
|
21
|
+
| dart | pubspec.lock, pubspec.yaml | Only for pubspec.lock |
|
|
22
|
+
| haskell | cabal.project.freeze | Yes |
|
|
23
|
+
| elixir | mix.lock | Yes |
|
|
24
|
+
| c/c++ | conan.lock, conanfile.txt | Yes only for conan.lock |
|
|
25
|
+
| clojure | Clojure CLI (deps.edn), Leiningen (project.clj) | Yes unless the files are parsed manually due to lack of clojure cli or leiningen command |
|
|
26
|
+
| swift | Package.resolved, Package.swift (swiftpm) | Yes |
|
|
27
|
+
| docker / oci image | All supported languages. Linux OS packages with plugins [4] | Best effort based on lock files |
|
|
28
|
+
| GitHub Actions | .github/workflows/\*.yml | N/A |
|
|
29
|
+
| Linux | All supported languages. Linux OS packages with plugins [5] | Best effort based on lock files |
|
|
30
|
+
| Windows | All supported languages. OS packages with best effort [5] | Best effort based on lock files |
|
|
31
|
+
| Jenkins Plugins | .hpi files | |
|
|
32
|
+
| Helm Charts | .yaml | N/A |
|
|
33
|
+
| Skaffold | .yaml | N/A |
|
|
34
|
+
| kustomization | .yaml | N/A |
|
|
35
|
+
| Tekton tasks | .yaml | N/A |
|
|
36
|
+
| Kubernetes | .yaml | N/A |
|
|
37
|
+
| Maven Cache | $HOME/.m2/repository/\*\*/\*.jar | N/A |
|
|
38
|
+
| SBT Cache | $HOME/.ivy2/cache/\*\*/\*.jar | N/A |
|
|
39
|
+
| Gradle Cache | $HOME/caches/modules-2/files-2.1/\*\*/\*.jar | N/A |
|
|
40
|
+
| Helm Index | $HOME/.cache/helm/repository/\*\*/\*.yaml | N/A |
|
|
41
|
+
| Docker compose | docker-compose\*.yml. Images would also be scanned. | N/A |
|
|
42
|
+
| Google CloudBuild configuration | cloudbuild.yaml | N/A |
|
|
43
|
+
| OpenAPI | openapi\*.json, openapi\*.yaml | N/A |
|
|
45
44
|
|
|
46
45
|
NOTE:
|
|
47
46
|
|
|
@@ -117,6 +116,10 @@ Options:
|
|
|
117
116
|
--server Run cdxgen as a server [boolean]
|
|
118
117
|
--server-host Listen address [default: "127.0.0.1"]
|
|
119
118
|
--server-port Listen port [default: "9090"]
|
|
119
|
+
--install-deps Install dependencies automatically for some
|
|
120
|
+
projects. Defaults to true but disabled for
|
|
121
|
+
containers and oci scans. Use --no-install-deps
|
|
122
|
+
to disable this feature.[boolean] [default: true]
|
|
120
123
|
--version Show version number [boolean]
|
|
121
124
|
-h Show help [boolean]
|
|
122
125
|
```
|
|
@@ -196,17 +199,6 @@ git clone https://github.com/cyclonedx/cdxgen.git
|
|
|
196
199
|
docker compose up
|
|
197
200
|
```
|
|
198
201
|
|
|
199
|
-
## Privado.ai support
|
|
200
|
-
|
|
201
|
-
In universal mode, cdxgen can look for any [Privado](https://www.privado.ai?utm_source=cyclonedx) scan reports and enrich the SBoM with data (flow and classification), endpoints, and leakage information. Such an SBoM would help with privacy compliance and use cases.
|
|
202
|
-
|
|
203
|
-
Invoke privado scan first to generate this report followed by an invocation of cdxgen in universal mode as shown.
|
|
204
|
-
|
|
205
|
-
```shell
|
|
206
|
-
privado scan --enable-javascript <directory>
|
|
207
|
-
cdxgen -t universal <directory> -o bom.json
|
|
208
|
-
```
|
|
209
|
-
|
|
210
202
|
## War file support
|
|
211
203
|
|
|
212
204
|
cdxgen can generate a BoM file from a given war file.
|
|
@@ -358,6 +350,24 @@ Permission to modify and redistribute is granted under the terms of the Apache 2
|
|
|
358
350
|
[license]: https://github.com/cyclonedx/cdxgen/blob/master/LICENSE
|
|
359
351
|
[cyclonedx-homepage]: https://cyclonedx.org
|
|
360
352
|
|
|
353
|
+
## Integration as library
|
|
354
|
+
|
|
355
|
+
This project is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) and requires Node.js >= 16
|
|
356
|
+
|
|
357
|
+
Minimal example:
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
import { createBom, submitBom } from "@cyclonedx/cdxgen";
|
|
361
|
+
// bomNSData would contain bomJson, bomXml
|
|
362
|
+
const bomNSData = await createBom(filePath, options);
|
|
363
|
+
// Submission to dependency track server
|
|
364
|
+
const dbody = await submitBom(args, bomNSData.bomXml);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Node.js >= 20 permission model
|
|
368
|
+
|
|
369
|
+
Refer to the [permissions document](./docs/PERMISSIONS.md)
|
|
370
|
+
|
|
361
371
|
## Contributing
|
|
362
372
|
|
|
363
373
|
Follow the usual PR process but prior to raising a PR run the following commands.
|
package/analyzer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import babelTraverse from "@babel/traverse";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { readdirSync, statSync, readFileSync } from "fs";
|
|
5
|
+
import { basename, resolve, isAbsolute } from "path";
|
|
6
6
|
|
|
7
7
|
const IGNORE_DIRS = [
|
|
8
8
|
"node_modules",
|
|
@@ -28,7 +28,7 @@ const IGNORE_FILE_PATTERN = new RegExp(
|
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
const getAllFiles = (dir, extn, files, result, regex) => {
|
|
31
|
-
files = files ||
|
|
31
|
+
files = files || readdirSync(dir);
|
|
32
32
|
result = result || [];
|
|
33
33
|
regex = regex || new RegExp(`\\${extn}$`);
|
|
34
34
|
|
|
@@ -37,9 +37,9 @@ const getAllFiles = (dir, extn, files, result, regex) => {
|
|
|
37
37
|
continue;
|
|
38
38
|
}
|
|
39
39
|
let file = join(dir, files[i]);
|
|
40
|
-
if (
|
|
40
|
+
if (statSync(file).isDirectory()) {
|
|
41
41
|
// Ignore directories
|
|
42
|
-
const dirName =
|
|
42
|
+
const dirName = basename(file);
|
|
43
43
|
if (
|
|
44
44
|
dirName.startsWith(".") ||
|
|
45
45
|
IGNORE_DIRS.includes(dirName.toLowerCase())
|
|
@@ -47,7 +47,7 @@ const getAllFiles = (dir, extn, files, result, regex) => {
|
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
49
|
try {
|
|
50
|
-
result = getAllFiles(file, extn,
|
|
50
|
+
result = getAllFiles(file, extn, readdirSync(file), result, regex);
|
|
51
51
|
} catch (error) {
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
@@ -95,11 +95,11 @@ const setFileRef = (allImports, file, pathway) => {
|
|
|
95
95
|
// replace relative imports with full path
|
|
96
96
|
let module = pathway;
|
|
97
97
|
if (/\.\//g.test(pathway) || /\.\.\//g.test(pathway)) {
|
|
98
|
-
module =
|
|
98
|
+
module = resolve(file, "..", pathway);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// initialise or increase reference count for file
|
|
102
|
-
if (
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(allImports, module)) {
|
|
103
103
|
allImports[module] = allImports[module] + 1;
|
|
104
104
|
} else {
|
|
105
105
|
allImports[module] = 1;
|
|
@@ -107,9 +107,9 @@ const setFileRef = (allImports, file, pathway) => {
|
|
|
107
107
|
|
|
108
108
|
// Handle module package name
|
|
109
109
|
// Eg: zone.js/dist/zone will be referred to as zone.js in package.json
|
|
110
|
-
if (!
|
|
110
|
+
if (!isAbsolute(module) && module.includes("/")) {
|
|
111
111
|
const modPkg = module.split("/")[0];
|
|
112
|
-
if (
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(allImports, modPkg)) {
|
|
113
113
|
allImports[modPkg] = allImports[modPkg] + 1;
|
|
114
114
|
} else {
|
|
115
115
|
allImports[modPkg] = 1;
|
|
@@ -122,10 +122,7 @@ const setFileRef = (allImports, file, pathway) => {
|
|
|
122
122
|
* references for any import, require or dynamic import files.
|
|
123
123
|
*/
|
|
124
124
|
const parseFileASTTree = (file, allImports) => {
|
|
125
|
-
const ast =
|
|
126
|
-
fs.readFileSync(file, "utf-8"),
|
|
127
|
-
babelParserOptions
|
|
128
|
-
);
|
|
125
|
+
const ast = parse(readFileSync(file, "utf-8"), babelParserOptions);
|
|
129
126
|
babelTraverse(ast, {
|
|
130
127
|
// Used for all ES6 import statements
|
|
131
128
|
ImportDeclaration: (path) => {
|
|
@@ -177,7 +174,7 @@ const getAllSrcJSAndTSFiles = (src) =>
|
|
|
177
174
|
/**
|
|
178
175
|
* Where Node CLI runs from.
|
|
179
176
|
*/
|
|
180
|
-
const findJSImports = async (src) => {
|
|
177
|
+
export const findJSImports = async (src) => {
|
|
181
178
|
const allImports = {};
|
|
182
179
|
const errFiles = [];
|
|
183
180
|
try {
|
|
@@ -195,4 +192,3 @@ const findJSImports = async (src) => {
|
|
|
195
192
|
return allImports;
|
|
196
193
|
}
|
|
197
194
|
};
|
|
198
|
-
exports.findJSImports = findJSImports;
|
package/bin/{cdxgen → cdxgen.js}
RENAMED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { createBom, submitBom } from "../index.js";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import jws from "jws";
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
import { start as _serverStart } from "../server.js";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import globalAgent from "global-agent";
|
|
12
|
+
import { table } from "table";
|
|
13
|
+
import process from "node:process";
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
let url = import.meta.url;
|
|
16
|
+
if (!url.startsWith("file://")) {
|
|
17
|
+
url = new URL(`file://${import.meta.url}`).toString();
|
|
18
|
+
}
|
|
19
|
+
const dirName = import.meta ? dirname(fileURLToPath(url)) : __dirname;
|
|
20
|
+
|
|
21
|
+
import yargs from "yargs";
|
|
22
|
+
import { hideBin } from "yargs/helpers";
|
|
23
|
+
|
|
24
|
+
const args = yargs(hideBin(process.argv))
|
|
11
25
|
.option("output", {
|
|
12
26
|
alias: "o",
|
|
13
27
|
description: "Output file for bom.xml or bom.json. Default bom.json"
|
|
@@ -87,13 +101,19 @@ const args = require("yargs")
|
|
|
87
101
|
description: "Listen port",
|
|
88
102
|
default: "9090"
|
|
89
103
|
})
|
|
104
|
+
.option("install-deps", {
|
|
105
|
+
type: "boolean",
|
|
106
|
+
default: true,
|
|
107
|
+
description:
|
|
108
|
+
"Install dependencies automatically for some projects. Defaults to true but disabled for containers and oci scans. Use --no-install-deps to disable this feature."
|
|
109
|
+
})
|
|
90
110
|
.scriptName("cdxgen")
|
|
91
111
|
.version()
|
|
92
112
|
.help("h").argv;
|
|
93
113
|
|
|
94
114
|
if (args.version) {
|
|
95
115
|
const packageJsonAsString = fs.readFileSync(
|
|
96
|
-
|
|
116
|
+
join(dirName, "..", "package.json"),
|
|
97
117
|
"utf-8"
|
|
98
118
|
);
|
|
99
119
|
const packageJson = JSON.parse(packageJsonAsString);
|
|
@@ -107,16 +127,15 @@ if (process.env.GLOBAL_AGENT_HTTP_PROXY || process.env.HTTP_PROXY) {
|
|
|
107
127
|
if (!process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE) {
|
|
108
128
|
process.env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = "";
|
|
109
129
|
}
|
|
110
|
-
const globalAgent = require("global-agent");
|
|
111
130
|
globalAgent.bootstrap();
|
|
112
131
|
}
|
|
113
132
|
|
|
114
133
|
let filePath = args._[0] || ".";
|
|
115
134
|
if (!args.projectName) {
|
|
116
135
|
if (filePath !== ".") {
|
|
117
|
-
args.projectName =
|
|
136
|
+
args.projectName = basename(filePath);
|
|
118
137
|
} else {
|
|
119
|
-
args.projectName =
|
|
138
|
+
args.projectName = basename(resolve(filePath));
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
|
|
@@ -125,12 +144,11 @@ if (!args.projectName) {
|
|
|
125
144
|
* multiProject: Boolean to indicate monorepo or multi-module projects
|
|
126
145
|
*/
|
|
127
146
|
let options = {
|
|
128
|
-
dev: true,
|
|
129
147
|
projectType: args.type,
|
|
130
148
|
multiProject: args.recurse,
|
|
131
149
|
output: args.output,
|
|
132
150
|
resolveClass: args.resolveClass,
|
|
133
|
-
installDeps:
|
|
151
|
+
installDeps: args.installDeps,
|
|
134
152
|
requiredOnly: args.requiredOnly,
|
|
135
153
|
failOnError: args.failOnError,
|
|
136
154
|
noBabel: args.noBabel || args.babel === false,
|
|
@@ -145,15 +163,51 @@ let options = {
|
|
|
145
163
|
serverPort: args.serverPort
|
|
146
164
|
};
|
|
147
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Check for node >= 20 permissions
|
|
168
|
+
*
|
|
169
|
+
* @param {string} filePath File path
|
|
170
|
+
* @returns
|
|
171
|
+
*/
|
|
172
|
+
const checkPermissions = (filePath) => {
|
|
173
|
+
if (!process.permission) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (!process.permission.has("fs.read", filePath)) {
|
|
177
|
+
console.log(
|
|
178
|
+
`FileSystemRead permission required. Please invoke with the argument --allow-fs-read="${resolve(
|
|
179
|
+
filePath
|
|
180
|
+
)}"`
|
|
181
|
+
);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (!process.permission.has("fs.write", tmpdir())) {
|
|
185
|
+
console.log(
|
|
186
|
+
`FileSystemWrite permission required. Please invoke with the argument --allow-fs-write="${tmpdir()}"`
|
|
187
|
+
);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (!process.permission.has("child")) {
|
|
191
|
+
console.log(
|
|
192
|
+
"ChildProcess permission is missing. This is required to spawn commands for some languages. Please invoke with the argument --allow-child-process"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
};
|
|
197
|
+
|
|
148
198
|
/**
|
|
149
199
|
* Method to start the bom creation process
|
|
150
200
|
*/
|
|
151
201
|
(async () => {
|
|
152
202
|
// Start SBoM server
|
|
153
203
|
if (args.server) {
|
|
154
|
-
return await
|
|
204
|
+
return await _serverStart(options);
|
|
205
|
+
}
|
|
206
|
+
// Check if cdxgen has the required permissions
|
|
207
|
+
if (!checkPermissions(filePath)) {
|
|
208
|
+
return;
|
|
155
209
|
}
|
|
156
|
-
const bomNSData = (await
|
|
210
|
+
const bomNSData = (await createBom(filePath, options)) || {};
|
|
157
211
|
if (!args.output) {
|
|
158
212
|
args.output = "bom.json";
|
|
159
213
|
args.print = true;
|
|
@@ -167,7 +221,7 @@ let options = {
|
|
|
167
221
|
} else {
|
|
168
222
|
const jsonFile = args.output.replace(".xml", ".json");
|
|
169
223
|
// Create bom json file
|
|
170
|
-
if (bomNSData.bomJson) {
|
|
224
|
+
if (!args.output.endsWith(".xml") && bomNSData.bomJson) {
|
|
171
225
|
let jsonPayload = undefined;
|
|
172
226
|
if (
|
|
173
227
|
typeof bomNSData.bomJson === "string" ||
|
|
@@ -194,9 +248,9 @@ let options = {
|
|
|
194
248
|
let privateKeyToUse = undefined;
|
|
195
249
|
let jwkPublicKey = undefined;
|
|
196
250
|
if (args.generateKeyAndSign) {
|
|
197
|
-
const
|
|
198
|
-
const publicKeyFile =
|
|
199
|
-
const privateKeyFile =
|
|
251
|
+
const jdirName = dirname(jsonFile);
|
|
252
|
+
const publicKeyFile = join(jdirName, "public.key");
|
|
253
|
+
const privateKeyFile = join(jdirName, "private.key");
|
|
200
254
|
const { privateKey, publicKey } = crypto.generateKeyPairSync(
|
|
201
255
|
"rsa",
|
|
202
256
|
{
|
|
@@ -266,9 +320,8 @@ let options = {
|
|
|
266
320
|
}
|
|
267
321
|
}
|
|
268
322
|
// Create bom xml file
|
|
269
|
-
if (bomNSData.bomXml) {
|
|
270
|
-
|
|
271
|
-
fs.writeFileSync(xmlFile, bomNSData.bomXml);
|
|
323
|
+
if (args.output.endsWith(".xml") && bomNSData.bomXml) {
|
|
324
|
+
fs.writeFileSync(args.output, bomNSData.bomXml);
|
|
272
325
|
}
|
|
273
326
|
//
|
|
274
327
|
if (bomNSData.nsMapping && Object.keys(bomNSData.nsMapping).length) {
|
|
@@ -291,8 +344,7 @@ let options = {
|
|
|
291
344
|
// Automatically submit the bom data
|
|
292
345
|
if (args.serverUrl && args.serverUrl != true && args.apiKey) {
|
|
293
346
|
try {
|
|
294
|
-
|
|
295
|
-
const dbody = await bom.submitBom(args, bomNSData.bomXml);
|
|
347
|
+
const dbody = await submitBom(args, bomNSData.bomJson);
|
|
296
348
|
console.log("Response from server", dbody);
|
|
297
349
|
} catch (err) {
|
|
298
350
|
console.log(err);
|
|
@@ -300,7 +352,6 @@ let options = {
|
|
|
300
352
|
}
|
|
301
353
|
|
|
302
354
|
if (args.print && bomNSData.bomJson && bomNSData.bomJson.components) {
|
|
303
|
-
const { table } = require("table");
|
|
304
355
|
const data = [["Group", "Name", "Version", "Scope"]];
|
|
305
356
|
for (let comp of bomNSData.bomJson.components) {
|
|
306
357
|
data.push([comp.group || "", comp.name, comp.version, comp.scope || ""]);
|