@cyclonedx/cdxgen 9.5.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
@@ -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
 
package/analyzer.js CHANGED
@@ -2,28 +2,37 @@ import { parse } from "@babel/parser";
2
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|config|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]);
@@ -87,72 +96,95 @@ const babelParserOptions = {
87
96
  * Filter only references to (t|jsx?) or (less|scss) files for now.
88
97
  * Opt to use our relative paths.
89
98
  */
90
- 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);
91
106
  // remove unexpected extension imports
92
107
  if (/\.(svg|png|jpg|d\.ts)/.test(pathway)) {
93
108
  return;
94
109
  }
95
-
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
+ };
96
121
  // replace relative imports with full path
97
- let module = pathway;
122
+ let moduleFullPath = pathway;
123
+ let wasAbsolute = false;
98
124
  if (/\.\//g.test(pathway) || /\.\.\//g.test(pathway)) {
99
- module = resolve(file, "..", pathway);
100
- }
101
-
102
- // initialise or increase reference count for file
103
- if (Object.prototype.hasOwnProperty.call(allImports, module)) {
104
- allImports[module] = allImports[module] + 1;
105
- } else {
106
- 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;
107
131
  }
108
-
132
+ allImports[moduleFullPath] = allImports[moduleFullPath] || new Set();
133
+ allImports[moduleFullPath].add(occurrence);
109
134
  // Handle module package name
110
135
  // Eg: zone.js/dist/zone will be referred to as zone.js in package.json
111
- if (!isAbsolute(module) && module.includes("/")) {
112
- const modPkg = module.split("/")[0];
113
- if (Object.prototype.hasOwnProperty.call(allImports, modPkg)) {
114
- allImports[modPkg] = allImports[modPkg] + 1;
115
- } else {
116
- allImports[modPkg] = 1;
117
- }
136
+ if (!wasAbsolute && moduleFullPath.includes("/")) {
137
+ const modPkg = moduleFullPath.split("/")[0];
138
+ allImports[modPkg] = allImports[modPkg] || new Set();
139
+ allImports[modPkg].add(occurrence);
118
140
  }
119
141
  };
120
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
+ });
168
+ }
169
+ return code;
170
+ };
171
+
121
172
  /**
122
173
  * Check AST tree for any (j|tsx?) files and set a file
123
174
  * references for any import, require or dynamic import files.
124
175
  */
125
- const parseFileASTTree = (file, allImports) => {
126
- const ast = parse(readFileSync(file, "utf-8"), babelParserOptions);
176
+ const parseFileASTTree = (src, file, allImports) => {
177
+ const ast = parse(fileToParseableCode(file), babelParserOptions);
127
178
  traverse.default(ast, {
128
- Import: (path) => {
129
- if (path && path.node) {
130
- setFileRef(allImports, file, path.node.source.value);
131
- }
132
- },
133
- ImportDefaultSpecifier: (path) => {
134
- if (path && path.node) {
135
- setFileRef(allImports, file, path.node.source.value);
136
- }
137
- },
138
- ImportNamespaceSpecifier: (path) => {
139
- if (path && path.node) {
140
- setFileRef(allImports, file, path.node.source.value);
141
- }
142
- },
143
- ImportAttribute: (path) => {
144
- if (path && path.node) {
145
- setFileRef(allImports, file, path.node.source.value);
146
- }
147
- },
148
- ImportOrExportDeclaration: (path) => {
149
- if (path && path.node) {
150
- setFileRef(allImports, file, path.node.source.value);
151
- }
152
- },
153
179
  ImportDeclaration: (path) => {
154
180
  if (path && path.node) {
155
- setFileRef(allImports, file, path.node.source.value);
181
+ setFileRef(
182
+ allImports,
183
+ src,
184
+ file,
185
+ path.node.source,
186
+ path.node.specifiers
187
+ );
156
188
  }
157
189
  },
158
190
  // For require('') statements
@@ -163,23 +195,29 @@ const parseFileASTTree = (file, allImports) => {
163
195
  path.node.name === "require" &&
164
196
  path.parent.type === "CallExpression"
165
197
  ) {
166
- setFileRef(allImports, file, path.parent.arguments[0].value);
198
+ setFileRef(allImports, src, file, path.parent.arguments[0]);
167
199
  }
168
200
  },
169
201
  // Use for dynamic imports like routes.jsx
170
202
  CallExpression: (path) => {
171
203
  if (path && path.node && path.node.callee.type === "Import") {
172
- setFileRef(allImports, file, path.node.arguments[0].value);
204
+ setFileRef(allImports, src, file, path.node.arguments[0]);
173
205
  }
174
206
  },
175
207
  // Use for export barrells
176
208
  ExportAllDeclaration: (path) => {
177
- setFileRef(allImports, file, path.node.source.value);
209
+ setFileRef(allImports, src, file, path.node.source);
178
210
  },
179
211
  ExportNamedDeclaration: (path) => {
180
212
  // ensure there is a path export
181
213
  if (path && path.node && path.node.source) {
182
- setFileRef(allImports, file, path.node.source.value);
214
+ setFileRef(
215
+ allImports,
216
+ src,
217
+ file,
218
+ path.node.source,
219
+ path.node.specifiers
220
+ );
183
221
  }
184
222
  }
185
223
  });
@@ -192,8 +230,12 @@ const getAllSrcJSAndTSFiles = (src) =>
192
230
  Promise.all([
193
231
  getAllFiles(src, ".js"),
194
232
  getAllFiles(src, ".jsx"),
233
+ getAllFiles(src, ".cjs"),
234
+ getAllFiles(src, ".mjs"),
195
235
  getAllFiles(src, ".ts"),
196
- getAllFiles(src, ".tsx")
236
+ getAllFiles(src, ".tsx"),
237
+ getAllFiles(src, ".vue"),
238
+ getAllFiles(src, ".svelte")
197
239
  ]);
198
240
 
199
241
  /**
@@ -207,7 +249,7 @@ export const findJSImports = async (src) => {
207
249
  const srcFiles = promiseMap.flatMap((d) => d);
208
250
  for (const file of srcFiles) {
209
251
  try {
210
- parseFileASTTree(file, allImports);
252
+ parseFileASTTree(src, file, allImports);
211
253
  } catch (err) {
212
254
  errFiles.push(file);
213
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 CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  // Evinse (Evinse Verification Is Nearly SBoM Evidence)
2
4
  import yargs from "yargs";
3
5
  import { hideBin } from "yargs/helpers";
@@ -65,7 +67,7 @@ const args = yargs(hideBin(process.argv))
65
67
  })
66
68
  .option("annotate", {
67
69
  description: "Include contents of atom slices as annotations",
68
- default: true,
70
+ default: false,
69
71
  type: "boolean"
70
72
  })
71
73
  .option("with-data-flow", {
package/bin/repl.js CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  import repl from "node:repl";
2
4
  import jsonata from "jsonata";
3
5
  import fs from "node:fs";
package/bin/verify.js CHANGED
@@ -5,6 +5,14 @@ import { hideBin } from "yargs/helpers";
5
5
  import fs from "node:fs";
6
6
  import jws from "jws";
7
7
  import process from "node:process";
8
+ import { fileURLToPath } from "node:url";
9
+ import { dirname, join } from "node:path";
10
+
11
+ let url = import.meta.url;
12
+ if (!url.startsWith("file://")) {
13
+ url = new URL(`file://${import.meta.url}`).toString();
14
+ }
15
+ const dirName = import.meta ? dirname(fileURLToPath(url)) : __dirname;
8
16
 
9
17
  const args = yargs(hideBin(process.argv))
10
18
  .option("input", {
@@ -20,8 +28,22 @@ const args = yargs(hideBin(process.argv))
20
28
  .version()
21
29
  .help("h").argv;
22
30
 
31
+ if (args.version) {
32
+ const packageJsonAsString = fs.readFileSync(
33
+ join(dirName, "..", "package.json"),
34
+ "utf-8"
35
+ );
36
+ const packageJson = JSON.parse(packageJsonAsString);
37
+
38
+ console.log(packageJson.version);
39
+ process.exit(0);
40
+ }
41
+
23
42
  const bomJson = JSON.parse(fs.readFileSync(args.input, "utf8"));
24
- const bomSignature = bomJson?.signature?.value;
43
+ const bomSignature =
44
+ bomJson.signature && bomJson.signature.value
45
+ ? bomJson.signature.value
46
+ : undefined;
25
47
  if (!bomSignature) {
26
48
  console.log("No signature was found!");
27
49
  } else {
package/display.js CHANGED
@@ -58,6 +58,19 @@ export const printServices = (bomJson) => {
58
58
  }
59
59
  };
60
60
 
61
+ const locationComparator = (a, b) => {
62
+ if (a && b && a.includes("#") && b.includes("#")) {
63
+ const tmpA = a.split("#");
64
+ const tmpB = b.split("#");
65
+ if (tmpA.length === 2 && tmpB.length === 2) {
66
+ if (tmpA[0] == tmpB[0]) {
67
+ return tmpA[1] - tmpB[1];
68
+ }
69
+ }
70
+ }
71
+ return a.localeCompare(b);
72
+ };
73
+
61
74
  export const printOccurrences = (bomJson) => {
62
75
  const data = [["Group", "Name", "Version", "Occurrences"]];
63
76
  if (!bomJson || !bomJson.components) {
@@ -73,7 +86,7 @@ export const printOccurrences = (bomJson) => {
73
86
  comp.version,
74
87
  comp.evidence.occurrences
75
88
  .map((l) => l.location)
76
- .sort()
89
+ .sort(locationComparator)
77
90
  .join("\n")
78
91
  ]);
79
92
  }
@@ -106,7 +119,7 @@ export const printCallStack = (bomJson) => {
106
119
  (c) => `${c.fullFilename}${c.line ? "#" + c.line : ""}`
107
120
  )
108
121
  )
109
- ).sort();
122
+ ).sort(locationComparator);
110
123
  let frameDisplay = [frames[0]];
111
124
  if (frames.length > 1) {
112
125
  for (let i = 1; i < frames.length - 1; i++) {