@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 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 | 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 | setup.py, requirements.txt [2], Pipfile.lock, poetry.lock, bdist_wheel, .whl, .egg-info | 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 |
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
- const babelParser = require("@babel/parser");
2
- const babelTraverse = require("@babel/traverse").default;
3
- const { join } = require("path");
4
- const fs = require("fs");
5
- const path = require("path");
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 || fs.readdirSync(dir);
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 (fs.statSync(file).isDirectory()) {
40
+ if (statSync(file).isDirectory()) {
41
41
  // Ignore directories
42
- const dirName = path.basename(file);
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, fs.readdirSync(file), result, regex);
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 = path.resolve(file, "..", pathway);
98
+ module = resolve(file, "..", pathway);
99
99
  }
100
100
 
101
101
  // initialise or increase reference count for file
102
- if (allImports.hasOwnProperty(module)) {
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 (!path.isAbsolute(module) && module.includes("/")) {
110
+ if (!isAbsolute(module) && module.includes("/")) {
111
111
  const modPkg = module.split("/")[0];
112
- if (allImports.hasOwnProperty(modPkg)) {
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 = babelParser.parse(
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;
@@ -1,13 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const bom = require("../index.js");
4
- const fs = require("fs");
5
- const path = require("path");
6
- const jws = require("jws");
7
- const crypto = require("crypto");
8
- const bomServer = require("../server.js");
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
- const args = require("yargs")
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
- path.join(__dirname, "../", "package.json"),
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 = path.basename(filePath);
136
+ args.projectName = basename(filePath);
118
137
  } else {
119
- args.projectName = path.basename(path.resolve(filePath));
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: true,
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 bomServer.start(options);
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 bom.createBom(filePath, options)) || {};
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 dirName = path.dirname(jsonFile);
198
- const publicKeyFile = path.join(dirName, "public.key");
199
- const privateKeyFile = path.join(dirName, "private.key");
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
- const xmlFile = args.output.replace(".json", ".xml");
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
- // TODO: Need to use json format for v9
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 || ""]);