@cyclonedx/cdxgen 9.3.2 → 9.4.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
@@ -10,6 +10,12 @@ NOTE:
10
10
 
11
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`.
12
12
 
13
+ ## Why cdxgen?
14
+
15
+ A typical application might comprise of several repos, components, and libraries linked together. Traditional techniques to generate a single SBoM per language or package manifest do not work in enterprise environments. So we built cdxgen - the universal polyglot SBoM generator!
16
+
17
+ <img src="./docs/why-cdxgen.jpg" alt="why cdxgen" width="256">
18
+
13
19
  ## Supported languages and package format
14
20
 
15
21
  | Language/Platform | Package format | Transitive dependencies |
@@ -61,7 +67,7 @@ Footnotes:
61
67
  - [4] - See section on plugins
62
68
  - [5] - Powered by osquery. See section on plugins
63
69
 
64
- ![cdxgen tree](./docs/cdxgen-tree.jpg)
70
+ <img src="./docs/cdxgen-tree.jpg" alt="cdxgen tree" width="256">
65
71
 
66
72
  ### Automatic usage detection
67
73
 
@@ -375,10 +381,33 @@ cdxgen can sign the generated SBoM json file to increase authenticity and non-re
375
381
  - SBOM_SIGN_PRIVATE_KEY: Location to the RSA private key
376
382
  - SBOM_SIGN_PUBLIC_KEY: Optional. Location to the RSA public key
377
383
 
378
- To generate test public/private key pairs, you can run cdxgen by passing the argument `--generate-key-and-sign`. The generated json file would have an attribute called `signature` which could be used for validation. [jwt.io](jwt.io) is a known site that could be used for such signature validation.
384
+ To generate test public/private key pairs, you can run cdxgen by passing the argument `--generate-key-and-sign`. The generated json file would have an attribute called `signature` which could be used for validation. [jwt.io](https://jwt.io) is a known site that could be used for such signature validation.
379
385
 
380
386
  ![SBoM signing](sbom-sign.jpg)
381
387
 
388
+ ### Verifying the signature (Node.js example)
389
+
390
+ There are many [libraries](https://jwt.io/#libraries-io) available to validate JSON Web Tokens. Below is a javascript example.
391
+
392
+ ```js
393
+ # npm install jws
394
+ const jws = require("jws");
395
+ const fs = require("fs");
396
+ // Location of the SBoM json file
397
+ const bomJsonFile = "bom.json";
398
+ // Location of the public key
399
+ const publicKeyFile = "public.key";
400
+ const bomJson = JSON.parse(fs.readFileSync(bomJsonFile, "utf8"));
401
+ // Retrieve the signature
402
+ const bomSignature = bomJson.signature.value;
403
+ const validationResult = jws.verify(bomSignature, bomJson.signature.algorithm, fs.readFileSync(publicKeyFile, "utf8"));
404
+ if (validationResult) {
405
+ console.log("Signature is valid!");
406
+ } else {
407
+ console.log("SBoM signature is invalid :(");
408
+ }
409
+ ```
410
+
382
411
  ## Automatic services detection
383
412
 
384
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.
@@ -414,6 +443,50 @@ const bomNSData = await createBom(filePath, options);
414
443
  const dbody = await submitBom(args, bomNSData.bomJson);
415
444
  ```
416
445
 
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
+
417
490
  ## Node.js >= 20 permission model
418
491
 
419
492
  Refer to the [permissions document](./docs/PERMISSIONS.md)
@@ -427,3 +500,7 @@ npm run lint
427
500
  npm run pretty
428
501
  npm test
429
502
  ```
503
+
504
+ ## Enterprise support
505
+
506
+ Enterprise support including custom development and integration services are available via AppThreat Ltd. Free community support is also available via [discord](https://discord.gg/tmmtjCEHNV).
package/bin/cdxgen.js CHANGED
@@ -266,9 +266,10 @@ const checkPermissions = (filePath) => {
266
266
  }
267
267
  let privateKeyToUse = undefined;
268
268
  let jwkPublicKey = undefined;
269
+ let publicKeyFile = undefined;
269
270
  if (args.generateKeyAndSign) {
270
271
  const jdirName = dirname(jsonFile);
271
- const publicKeyFile = join(jdirName, "public.key");
272
+ publicKeyFile = join(jdirName, "public.key");
272
273
  const privateKeyFile = join(jdirName, "private.key");
273
274
  const { privateKey, publicKey } = crypto.generateKeyPairSync(
274
275
  "rsa",
@@ -331,6 +332,26 @@ const checkPermissions = (filePath) => {
331
332
  jsonFile,
332
333
  JSON.stringify(bomJsonUnsignedObj, null, 2)
333
334
  );
335
+ if (publicKeyFile) {
336
+ // Verifying this signature
337
+ const signatureVerification = jws.verify(
338
+ signature,
339
+ alg,
340
+ fs.readFileSync(publicKeyFile, "utf8")
341
+ );
342
+ if (signatureVerification) {
343
+ console.log(
344
+ "SBoM signature is verifiable with the public key and the algorithm",
345
+ publicKeyFile,
346
+ alg
347
+ );
348
+ } else {
349
+ console.log("SBoM signature verification was unsuccessful");
350
+ console.log(
351
+ "Check if the public key was exported in PEM format"
352
+ );
353
+ }
354
+ }
334
355
  }
335
356
  } catch (ex) {
336
357
  console.log("SBoM signing was unsuccessful", ex);
package/bin/repl.js ADDED
@@ -0,0 +1,311 @@
1
+ import repl from "node:repl";
2
+ import jsonata from "jsonata";
3
+ import fs from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import process from "node:process";
7
+
8
+ import { createBom } from "../index.js";
9
+ import { validateBom } from "../validator.js";
10
+ import { printTable, printDependencyTree } from "../display.js";
11
+
12
+ const options = {
13
+ useColors: true,
14
+ breakEvalOnSigint: true,
15
+ preview: true,
16
+ prompt: "↝ ",
17
+ ignoreUndefined: true,
18
+ useGlobal: true
19
+ };
20
+
21
+ const cdxArt = ` ██████╗██████╗ ██╗ ██╗
22
+ ██╔════╝██╔══██╗╚██╗██╔╝
23
+ ██║ ██║ ██║ ╚███╔╝
24
+ ██║ ██║ ██║ ██╔██╗
25
+ ╚██████╗██████╔╝██╔╝ ██╗
26
+ ╚═════╝╚═════╝ ╚═╝ ╚═╝
27
+ `;
28
+
29
+ console.log("\n" + cdxArt);
30
+
31
+ // The current sbom is stored here
32
+ let sbom = undefined;
33
+
34
+ let historyFile = undefined;
35
+ const historyConfigDir = join(homedir(), ".config", ".cdxgen");
36
+ if (!process.env.CDXGEN_REPL_HISTORY && !fs.existsSync(historyConfigDir)) {
37
+ try {
38
+ fs.mkdirSync(historyConfigDir, { recursive: true });
39
+ historyFile = join(historyConfigDir, ".repl_history");
40
+ } catch (e) {
41
+ // ignore
42
+ }
43
+ } else {
44
+ historyFile = join(historyConfigDir, ".repl_history");
45
+ }
46
+
47
+ export const importSbom = (sbomOrPath) => {
48
+ if (sbomOrPath && sbomOrPath.endsWith(".json") && fs.existsSync(sbomOrPath)) {
49
+ try {
50
+ sbom = JSON.parse(fs.readFileSync(sbomOrPath, "utf-8"));
51
+ console.log(`✅ SBoM imported successfully from ${sbomOrPath}`);
52
+ } catch (e) {
53
+ console.log(
54
+ `⚠ Unable to import the SBoM from ${sbomOrPath} due to ${e}`
55
+ );
56
+ }
57
+ } else {
58
+ console.log(`⚠ ${sbomOrPath} is invalid.`);
59
+ }
60
+ };
61
+ // Load any sbom passed from the command line
62
+ if (process.argv.length > 2) {
63
+ importSbom(process.argv[process.argv.length - 1]);
64
+ console.log("💭 Type .print to view the SBoM as a table");
65
+ } else if (fs.existsSync("bom.json")) {
66
+ // If the current directory has a bom.json load it
67
+ importSbom("bom.json");
68
+ } else {
69
+ console.log("💭 Use .create <path> to create an SBoM for the given path.");
70
+ console.log("💭 Use .import <json> to import an existing SBoM.");
71
+ console.log("💭 Type .exit or press ctrl+d to close.");
72
+ }
73
+
74
+ const cdxgenRepl = repl.start(options);
75
+ if (historyFile) {
76
+ cdxgenRepl.setupHistory(
77
+ process.env.CDXGEN_REPL_HISTORY || historyFile,
78
+ (err) => {
79
+ if (err) {
80
+ console.log(
81
+ "⚠ REPL history would not be persisted for this session. Set the environment variable CDXGEN_REPL_HISTORY to specify a custom history file"
82
+ );
83
+ }
84
+ }
85
+ );
86
+ }
87
+ cdxgenRepl.defineCommand("create", {
88
+ help: "create an SBoM for the given path",
89
+ async action(sbomOrPath) {
90
+ this.clearBufferedCommand();
91
+ const tempDir = fs.mkdtempSync(join(tmpdir(), "cdxgen-repl-"));
92
+ const bomFile = join(tempDir, "bom.json");
93
+ const bomNSData = await createBom(sbomOrPath, {
94
+ multiProject: true,
95
+ installDeps: true,
96
+ output: bomFile
97
+ });
98
+ if (bomNSData) {
99
+ sbom = bomNSData.bomJson;
100
+ console.log("✅ SBoM imported successfully.");
101
+ console.log("💭 Type .print to view the SBoM as a table");
102
+ } else {
103
+ console.log("SBoM was not generated successfully");
104
+ }
105
+ this.displayPrompt();
106
+ }
107
+ });
108
+ cdxgenRepl.defineCommand("import", {
109
+ help: "import an existing SBoM",
110
+ action(sbomOrPath) {
111
+ this.clearBufferedCommand();
112
+ importSbom(sbomOrPath);
113
+ this.displayPrompt();
114
+ }
115
+ });
116
+ cdxgenRepl.defineCommand("exit", {
117
+ help: "exit",
118
+ action() {
119
+ this.close();
120
+ }
121
+ });
122
+ cdxgenRepl.defineCommand("sbom", {
123
+ help: "show the current sbom",
124
+ action() {
125
+ if (sbom) {
126
+ console.log(sbom);
127
+ } else {
128
+ console.log(
129
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
130
+ );
131
+ }
132
+ this.displayPrompt();
133
+ }
134
+ });
135
+ cdxgenRepl.defineCommand("search", {
136
+ help: "search the current sbom",
137
+ async action(searchStr) {
138
+ if (sbom) {
139
+ if (searchStr) {
140
+ try {
141
+ if (!searchStr.includes("~>")) {
142
+ searchStr = `components[group ~> /${searchStr}/i or name ~> /${searchStr}/i or description ~> /${searchStr}/i or publisher ~> /${searchStr}/i or purl ~> /${searchStr}/i]`;
143
+ }
144
+ const expression = jsonata(searchStr);
145
+ let components = await expression.evaluate(sbom);
146
+ if (!components) {
147
+ console.log("No results found!");
148
+ } else {
149
+ printTable({ components, dependencies: [] });
150
+ }
151
+ } catch (e) {
152
+ console.log(e);
153
+ }
154
+ } else {
155
+ console.log(
156
+ "⚠ Specify the search string. Eg: .search <search string>"
157
+ );
158
+ }
159
+ } else {
160
+ console.log(
161
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
162
+ );
163
+ }
164
+ this.displayPrompt();
165
+ }
166
+ });
167
+ cdxgenRepl.defineCommand("sort", {
168
+ help: "sort the current sbom based on the attribute",
169
+ async action(sortStr) {
170
+ if (sbom) {
171
+ if (sortStr) {
172
+ try {
173
+ if (!sortStr.includes("^")) {
174
+ sortStr = `components^(${sortStr})`;
175
+ }
176
+ const expression = jsonata(sortStr);
177
+ let components = await expression.evaluate(sbom);
178
+ if (!components) {
179
+ console.log("No results found!");
180
+ } else {
181
+ printTable({ components, dependencies: [] });
182
+ // Store the sorted list in memory
183
+ if (components.length === sbom.components.length) {
184
+ sbom.components = components;
185
+ }
186
+ }
187
+ } catch (e) {
188
+ console.log(e);
189
+ }
190
+ } else {
191
+ console.log("⚠ Specify the attribute to sort by. Eg: .sort name");
192
+ }
193
+ } else {
194
+ console.log(
195
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
196
+ );
197
+ }
198
+ this.displayPrompt();
199
+ }
200
+ });
201
+ cdxgenRepl.defineCommand("query", {
202
+ help: "query the current sbom",
203
+ async action(querySpec) {
204
+ if (sbom) {
205
+ if (querySpec) {
206
+ try {
207
+ const expression = jsonata(querySpec);
208
+ console.log(await expression.evaluate(sbom));
209
+ } catch (e) {
210
+ console.log(e);
211
+ }
212
+ } else {
213
+ console.log(
214
+ "⚠ Specify the search specification in jsonata format. Eg: .query metadata.component"
215
+ );
216
+ }
217
+ } else {
218
+ console.log(
219
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
220
+ );
221
+ }
222
+ this.displayPrompt();
223
+ }
224
+ });
225
+ cdxgenRepl.defineCommand("print", {
226
+ help: "print the current sbom as a table",
227
+ action() {
228
+ if (sbom) {
229
+ printTable(sbom);
230
+ } else {
231
+ console.log(
232
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
233
+ );
234
+ }
235
+ this.displayPrompt();
236
+ }
237
+ });
238
+ cdxgenRepl.defineCommand("tree", {
239
+ help: "display the dependency tree",
240
+ action() {
241
+ if (sbom) {
242
+ printDependencyTree(sbom);
243
+ } else {
244
+ console.log(
245
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
246
+ );
247
+ }
248
+ this.displayPrompt();
249
+ }
250
+ });
251
+ cdxgenRepl.defineCommand("validate", {
252
+ help: "validate the sbom",
253
+ action() {
254
+ if (sbom) {
255
+ const result = validateBom(sbom);
256
+ if (result) {
257
+ console.log("SBoM is valid!");
258
+ }
259
+ } else {
260
+ console.log(
261
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
262
+ );
263
+ }
264
+ this.displayPrompt();
265
+ }
266
+ });
267
+ cdxgenRepl.defineCommand("save", {
268
+ help: "save the sbom to a new file",
269
+ action(saveToFile) {
270
+ if (sbom) {
271
+ if (!saveToFile) {
272
+ saveToFile = "bom.json";
273
+ }
274
+ fs.writeFileSync(saveToFile, JSON.stringify(sbom, null, 2));
275
+ console.log(`SBoM saved successfully to ${saveToFile}`);
276
+ } else {
277
+ console.log(
278
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
279
+ );
280
+ }
281
+ this.displayPrompt();
282
+ }
283
+ });
284
+ cdxgenRepl.defineCommand("update", {
285
+ help: "update the sbom components based on the given query",
286
+ async action(updateSpec) {
287
+ if (sbom) {
288
+ if (!updateSpec) {
289
+ return;
290
+ }
291
+ if (!updateSpec.startsWith("|")) {
292
+ updateSpec = "|" + updateSpec;
293
+ }
294
+ if (!updateSpec.endsWith("|")) {
295
+ updateSpec = updateSpec + "|";
296
+ }
297
+ updateSpec = "$ ~> " + updateSpec;
298
+ const expression = jsonata(updateSpec);
299
+ const newSbom = await expression.evaluate(sbom);
300
+ if (newSbom && newSbom.components.length <= sbom.components.length) {
301
+ sbom = newSbom;
302
+ }
303
+ console.log("SBoM updated successfully.");
304
+ } else {
305
+ console.log(
306
+ "⚠ No SBoM is loaded. Use .import command to import an existing SBoM"
307
+ );
308
+ }
309
+ this.displayPrompt();
310
+ }
311
+ });
package/display.js CHANGED
@@ -13,6 +13,9 @@ const MAX_TREE_DEPTH = 3;
13
13
 
14
14
  export const printTable = (bomJson) => {
15
15
  const data = [["Group", "Name", "Version", "Scope"]];
16
+ if (!bomJson || !bomJson.components) {
17
+ return;
18
+ }
16
19
  for (const comp of bomJson.components) {
17
20
  data.push([comp.group || "", comp.name, comp.version, comp.scope || ""]);
18
21
  }
package/docker.js CHANGED
@@ -24,7 +24,7 @@ import { x } from "tar";
24
24
  import { spawnSync } from "node:child_process";
25
25
  import { DEBUG_MODE } from "./utils.js";
26
26
 
27
- const isWin = _platform() === "win32";
27
+ export const isWin = _platform() === "win32";
28
28
 
29
29
  let dockerConn = undefined;
30
30
  let isPodman = false;
package/docker.test.js CHANGED
@@ -3,14 +3,17 @@ import {
3
3
  parseImageName,
4
4
  getImage,
5
5
  removeImage,
6
- exportImage
6
+ exportImage,
7
+ isWin
7
8
  } from "./docker.js";
8
9
  import { expect, test } from "@jest/globals";
9
10
 
10
11
  test("docker connection", async () => {
11
- const dockerConn = await getConnection();
12
- expect(dockerConn);
13
- });
12
+ if (!(isWin && process.env.CI === "true")) {
13
+ const dockerConn = await getConnection();
14
+ expect(dockerConn);
15
+ }
16
+ }, 120000);
14
17
 
15
18
  test("parseImageName tests", () => {
16
19
  expect(parseImageName("debian")).toEqual({
@@ -59,7 +62,7 @@ test("parseImageName tests", () => {
59
62
  digest: "5d008306a7c5d09ba0161a3408fa3839dc2c9dd991ffb68adecc1040399fe9e1",
60
63
  platform: ""
61
64
  });
62
- });
65
+ }, 120000);
63
66
 
64
67
  test("docker getImage", async () => {
65
68
  const imageData = await getImage("hello-world:latest");
package/index.js CHANGED
@@ -199,18 +199,25 @@ const createDefaultParentComponent = (path, type = "application") => {
199
199
 
200
200
  const determineParentComponent = (options) => {
201
201
  let parentComponent = undefined;
202
- if (options.projectName && options.projectVersion) {
202
+ if (options.parentComponent && Object.keys(options.parentComponent).length) {
203
+ return options.parentComponent;
204
+ } else if (options.projectName && options.projectVersion) {
203
205
  parentComponent = {
204
206
  group: options.projectGroup || "",
205
207
  name: options.projectName,
206
208
  version: "" + options.projectVersion || "",
207
209
  type: "application"
208
210
  };
209
- } else if (
210
- options.parentComponent &&
211
- Object.keys(options.parentComponent).length
212
- ) {
213
- return options.parentComponent;
211
+ const ppurl = new PackageURL(
212
+ parentComponent.type,
213
+ parentComponent.group,
214
+ parentComponent.name,
215
+ parentComponent.version,
216
+ null,
217
+ null
218
+ ).toString();
219
+ parentComponent["bom-ref"] = ppurl;
220
+ parentComponent["purl"] = decodeURIComponent(ppurl);
214
221
  }
215
222
  return parentComponent;
216
223
  };
@@ -323,11 +330,18 @@ function addMetadata(parentComponent = {}, format = "xml", options = {}) {
323
330
  if (parentComponent) {
324
331
  delete parentComponent.evidence;
325
332
  delete parentComponent._integrity;
333
+ delete parentComponent.license;
334
+ if (!parentComponent["purl"] && parentComponent["bom-ref"]) {
335
+ parentComponent["purl"] = decodeURIComponent(
336
+ parentComponent["bom-ref"]
337
+ );
338
+ }
326
339
  }
327
340
  if (parentComponent && parentComponent.components) {
328
341
  for (const comp of parentComponent.components) {
329
342
  delete comp.evidence;
330
343
  delete comp._integrity;
344
+ delete comp.license;
331
345
  if (!comp["bom-ref"] && comp.name && comp.type) {
332
346
  let fullName =
333
347
  comp.group && comp.group.length
@@ -1336,7 +1350,7 @@ export const createJavaBom = async (path, options) => {
1336
1350
  let gradleDepArgs = [
1337
1351
  sp.purl === parentComponent.purl
1338
1352
  ? depTaskWithArgs[0]
1339
- : `:${sp.name}:${depTaskWithArgs[0]}`
1353
+ : `:${sp.name.replace(/\//, ":")}:${depTaskWithArgs[0]}`
1340
1354
  ];
1341
1355
  gradleDepArgs = gradleDepArgs
1342
1356
  .concat(depTaskWithArgs.slice(1))
@@ -1786,9 +1800,9 @@ export const createNodejsBom = async (path, options) => {
1786
1800
  parentComponent.type = "application";
1787
1801
  ppurl = new PackageURL(
1788
1802
  "npm",
1789
- parentComponent.group,
1790
- parentComponent.name,
1791
- parentComponent.version,
1803
+ options.projectGroup || parentComponent.group,
1804
+ options.projectName || parentComponent.name,
1805
+ options.projectVersion || parentComponent.version,
1792
1806
  null,
1793
1807
  null
1794
1808
  ).toString();
@@ -1806,9 +1820,9 @@ export const createNodejsBom = async (path, options) => {
1806
1820
  };
1807
1821
  ppurl = new PackageURL(
1808
1822
  "npm",
1809
- parentComponent.group,
1810
- parentComponent.name,
1811
- parentComponent.version,
1823
+ options.projectGroup || parentComponent.group,
1824
+ options.projectName || parentComponent.name,
1825
+ options.projectVersion || parentComponent.version,
1812
1826
  null,
1813
1827
  null
1814
1828
  ).toString();
@@ -1837,11 +1851,10 @@ export const createNodejsBom = async (path, options) => {
1837
1851
  console.log(`Parsing ${f}`);
1838
1852
  }
1839
1853
  // Parse package-lock.json if available
1840
- const parsedList = await parsePkgLock(f);
1854
+ const parsedList = await parsePkgLock(f, options);
1841
1855
  const dlist = parsedList.pkgList;
1842
1856
  const tmpParentComponent = dlist.splice(0, 1)[0] || {};
1843
1857
  tmpParentComponent.type = "application";
1844
- // Create a default parent component based on directory name
1845
1858
  if (!Object.keys(parentComponent).length) {
1846
1859
  parentComponent = tmpParentComponent;
1847
1860
  } else {
@@ -1929,9 +1942,9 @@ export const createNodejsBom = async (path, options) => {
1929
1942
  tmpParentComponent.type = "application";
1930
1943
  ppurl = new PackageURL(
1931
1944
  "npm",
1932
- tmpParentComponent.group,
1933
- tmpParentComponent.name,
1934
- tmpParentComponent.version,
1945
+ options.projectGroup || tmpParentComponent.group,
1946
+ options.projectName || tmpParentComponent.name,
1947
+ options.projectVersion || tmpParentComponent.version,
1935
1948
  null,
1936
1949
  null
1937
1950
  ).toString();
@@ -1948,15 +1961,15 @@ export const createNodejsBom = async (path, options) => {
1948
1961
  const tmpA = dirName.split(sep);
1949
1962
  dirName = tmpA[tmpA.length - 1];
1950
1963
  const tmpParentComponent = {
1951
- group: "",
1952
- name: dirName,
1964
+ group: options.projectGroup || "",
1965
+ name: options.projectName || dirName,
1953
1966
  type: "application"
1954
1967
  };
1955
1968
  ppurl = new PackageURL(
1956
1969
  "npm",
1957
1970
  tmpParentComponent.group,
1958
1971
  tmpParentComponent.name,
1959
- tmpParentComponent.version,
1972
+ options.projectVersion || tmpParentComponent.version,
1960
1973
  null,
1961
1974
  null
1962
1975
  ).toString();
@@ -2030,6 +2043,8 @@ export const createNodejsBom = async (path, options) => {
2030
2043
  if (parentSubComponents.length) {
2031
2044
  parentComponent.components = parentSubComponents;
2032
2045
  }
2046
+ // We need to set this to force our version to be used rather than the directory name based one.
2047
+ options.parentComponent = parentComponent;
2033
2048
  return buildBomNSData(options, pkgList, "npm", {
2034
2049
  allImports,
2035
2050
  src: path,
@@ -2260,14 +2275,20 @@ export const createPythonBom = async (path, options) => {
2260
2275
  pkgMap = getPipFrozenTree(path, undefined, tempDir);
2261
2276
  }
2262
2277
  // Get the imported modules and a dedupe list of packages
2263
- const parentDependsOn = [];
2278
+ const parentDependsOn = new Set();
2264
2279
  const retMap = await getPyModules(path, pkgList);
2265
2280
  if (retMap.pkgList && retMap.pkgList.length) {
2266
2281
  pkgList = pkgList.concat(retMap.pkgList);
2267
2282
  for (const p of retMap.pkgList) {
2268
- if (p.version) {
2269
- parentDependsOn.push(`pkg:pypi/${p.name}@${p.version}`);
2283
+ if (
2284
+ !p.version ||
2285
+ (parentComponent &&
2286
+ p.name === parentComponent.name &&
2287
+ (p.version === parentComponent.version || p.version === "latest"))
2288
+ ) {
2289
+ continue;
2270
2290
  }
2291
+ parentDependsOn.add(`pkg:pypi/${p.name}@${p.version}`);
2271
2292
  }
2272
2293
  }
2273
2294
  if (retMap.dependenciesList) {
@@ -2285,11 +2306,11 @@ export const createPythonBom = async (path, options) => {
2285
2306
  if (
2286
2307
  parentComponent &&
2287
2308
  p.name === parentComponent.name &&
2288
- p.version === parentComponent.version
2309
+ (p.version === parentComponent.version || p.version === "latest")
2289
2310
  ) {
2290
2311
  continue;
2291
2312
  }
2292
- parentDependsOn.push(`pkg:pypi/${p.name}@${p.version}`);
2313
+ parentDependsOn.add(`pkg:pypi/${p.name}@${p.version}`);
2293
2314
  }
2294
2315
  if (pkgMap.pkgList && pkgMap.pkgList.length) {
2295
2316
  pkgList = pkgList.concat(pkgMap.pkgList);
@@ -2301,13 +2322,22 @@ export const createPythonBom = async (path, options) => {
2301
2322
  parentComponent
2302
2323
  );
2303
2324
  }
2304
- const pdependencies = {
2305
- ref: parentComponent["bom-ref"],
2306
- dependsOn: parentDependsOn.filter(
2307
- (r) => parentComponent && r !== parentComponent["bom-ref"]
2308
- )
2309
- };
2310
- dependencies.splice(0, 0, pdependencies);
2325
+ let parentPresent = false;
2326
+ for (const d of dependencies) {
2327
+ if (d.ref === parentComponent["bom-ref"]) {
2328
+ parentPresent = true;
2329
+ break;
2330
+ }
2331
+ }
2332
+ if (!parentPresent) {
2333
+ const pdependencies = {
2334
+ ref: parentComponent["bom-ref"],
2335
+ dependsOn: Array.from(parentDependsOn).filter(
2336
+ (r) => parentComponent && r !== parentComponent["bom-ref"]
2337
+ )
2338
+ };
2339
+ dependencies.splice(0, 0, pdependencies);
2340
+ }
2311
2341
  }
2312
2342
  }
2313
2343
  // Final fallback is to manually parse setup.py if we still
@@ -3736,7 +3766,9 @@ export const mergeDependencies = (
3736
3766
  parentComponent = {}
3737
3767
  ) => {
3738
3768
  if (!parentComponent && DEBUG_MODE) {
3739
- console.log("Unable to determine parent component. Dependencies will be flattened.");
3769
+ console.log(
3770
+ "Unable to determine parent component. Dependencies will be flattened."
3771
+ );
3740
3772
  }
3741
3773
  const deps_map = {};
3742
3774
  const parentRef =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyclonedx/cdxgen",
3
- "version": "9.3.2",
3
+ "version": "9.4.0",
4
4
  "description": "Creates CycloneDX Software Bill-of-Materials (SBOM) from source or container image",
5
5
  "homepage": "http://github.com/cyclonedx/cdxgen",
6
6
  "author": "Prabhu Subramanian <prabhu@appthreat.com>",
@@ -31,13 +31,14 @@
31
31
  "type": "module",
32
32
  "exports": "./index.js",
33
33
  "bin": {
34
- "cdxgen": "./bin/cdxgen.js"
34
+ "cdxgen": "./bin/cdxgen.js",
35
+ "cdxi": "./bin/repl.js"
35
36
  },
36
37
  "scripts": {
37
38
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --inject-globals false",
38
39
  "watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --inject-globals false",
39
- "lint": "eslint *.js *.test.js bin/cdxgen.js",
40
- "pretty": "prettier --write *.js data/*.json bin/cdxgen.js --trailing-comma=none"
40
+ "lint": "eslint *.js *.test.js bin/*.js",
41
+ "pretty": "prettier --write *.js data/*.json bin/*.js --trailing-comma=none"
41
42
  },
42
43
  "engines": {
43
44
  "node": ">=16"
@@ -50,8 +51,8 @@
50
51
  "url": "https://github.com/cyclonedx/cdxgen/issues"
51
52
  },
52
53
  "dependencies": {
53
- "@babel/parser": "^7.22.5",
54
- "@babel/traverse": "^7.22.5",
54
+ "@babel/parser": "^7.22.10",
55
+ "@babel/traverse": "^7.22.10",
55
56
  "ajv": "^8.12.0",
56
57
  "ajv-formats": "^2.1.1",
57
58
  "cheerio": "^1.0.0-rc.12",
@@ -75,11 +76,12 @@
75
76
  "yargs": "^17.7.2"
76
77
  },
77
78
  "optionalDependencies": {
78
- "@appthreat/atom": "^1.0.0",
79
+ "@appthreat/atom": "^1.0.1",
79
80
  "@cyclonedx/cdxgen-plugins-bin": "^1.2.0",
80
81
  "body-parser": "^1.20.2",
81
82
  "compression": "^1.7.4",
82
- "connect": "^3.7.0"
83
+ "connect": "^3.7.0",
84
+ "jsonata": "^2.0.3"
83
85
  },
84
86
  "files": [
85
87
  "*.js",
@@ -88,7 +90,8 @@
88
90
  ],
89
91
  "devDependencies": {
90
92
  "caxa": "^3.0.1",
91
- "eslint": "^8.43.0",
92
- "jest": "^29.5.0"
93
+ "eslint": "^8.47.0",
94
+ "jest": "^29.5.0",
95
+ "prettier": "3.0.1"
93
96
  }
94
97
  }
package/piptree.js CHANGED
@@ -57,12 +57,11 @@ def find_deps(idx, visited, reqs, traverse_count):
57
57
  if not d:
58
58
  continue
59
59
  r.project_name = d.project_name if d is not None else r.project_name
60
- if len(visited) > 100 and visited.get(r.project_name):
60
+ if len(visited) > 100 or visited.get(r.project_name, 0) > 5:
61
61
  return freqs
62
62
  specs = sorted(r.specs, reverse=True)
63
63
  specs_str = ",".join(["".join(sp) for sp in specs]) if specs else ""
64
64
  dreqs = d.requires()
65
- visited[r.project_name] = True
66
65
  freqs.append(
67
66
  {
68
67
  "name": r.project_name,
@@ -71,6 +70,7 @@ def find_deps(idx, visited, reqs, traverse_count):
71
70
  "dependencies": find_deps(idx, visited, dreqs, traverse_count + 1) if dreqs and traverse_count < 200 else [],
72
71
  }
73
72
  )
73
+ visited[r.project_name] = visited.get(r.project_name, 0) + 1
74
74
  return freqs
75
75
 
76
76
 
package/utils.js CHANGED
@@ -497,12 +497,16 @@ export const parsePkgJson = async (pkgJsonFile) => {
497
497
  * Parse nodejs package lock file
498
498
  *
499
499
  * @param {string} pkgLockFile package-lock.json file
500
+ * @param {object} options Command line options
500
501
  */
501
- export const parsePkgLock = async (pkgLockFile) => {
502
+ export const parsePkgLock = async (pkgLockFile, options = {}) => {
502
503
  let pkgList = [];
503
504
  const dependenciesList = [];
504
505
  const depKeys = {};
505
506
  let rootPkg = {};
507
+ if (!options) {
508
+ options = {};
509
+ }
506
510
  if (existsSync(pkgLockFile)) {
507
511
  const lockData = JSON.parse(readFileSync(pkgLockFile, "utf8"));
508
512
  rootPkg.name = lockData.name || "";
@@ -510,16 +514,16 @@ export const parsePkgLock = async (pkgLockFile) => {
510
514
  if (lockData.name && lockData.packages && lockData.packages[""]) {
511
515
  // Build the initial dependency tree for the root package
512
516
  rootPkg = {
513
- group: "",
514
- name: lockData.name,
515
- version: lockData.version,
517
+ group: options.projectGroup || "",
518
+ name: options.projectName || lockData.name,
519
+ version: options.projectVersion || lockData.version,
516
520
  type: "application",
517
521
  "bom-ref": decodeURIComponent(
518
522
  new PackageURL(
519
523
  "npm",
520
- "",
521
- lockData.name,
522
- lockData.version,
524
+ options.projectGroup || "",
525
+ options.projectName || lockData.name,
526
+ options.projectVersion || lockData.version,
523
527
  null,
524
528
  null
525
529
  ).toString()
@@ -531,10 +535,10 @@ export const parsePkgLock = async (pkgLockFile) => {
531
535
  dirName = tmpA[tmpA.length - 1];
532
536
  // v1 lock file
533
537
  rootPkg = {
534
- group: "",
535
- name: lockData.name || dirName,
536
- version: lockData.version || "",
537
- type: "application"
538
+ group: options.projectGroup || "",
539
+ name: options.projectName || lockData.name || dirName,
540
+ version: options.projectVersion || lockData.version || "",
541
+ type: "npm"
538
542
  };
539
543
  }
540
544
  if (rootPkg && rootPkg.name) {
@@ -625,6 +629,7 @@ export const yarnLockToIdentMap = function (lockData) {
625
629
  const identMap = {};
626
630
  let currentIdents = [];
627
631
  lockData.split("\n").forEach((l) => {
632
+ l = l.replace("\r", "");
628
633
  if (l === "\n" || l.startsWith("#")) {
629
634
  return;
630
635
  }
@@ -687,6 +692,7 @@ export const parseYarnLock = async function (yarnLockFile) {
687
692
  const identMap = yarnLockToIdentMap(lockData);
688
693
  let prefixAtSymbol = false;
689
694
  lockData.split("\n").forEach((l) => {
695
+ l = l.replace("\r", "");
690
696
  if (l === "\n" || l.startsWith("#")) {
691
697
  return;
692
698
  }
@@ -1664,7 +1670,7 @@ export const parseGradleProjects = function (rawOutput) {
1664
1670
  if (typeof rawOutput === "string") {
1665
1671
  const tmpA = rawOutput.split("\n");
1666
1672
  tmpA.forEach((l) => {
1667
- l = l.replace("\r", "")
1673
+ l = l.replace("\r", "");
1668
1674
  if (l.startsWith("Root project ")) {
1669
1675
  rootProject = l
1670
1676
  .split("Root project ")[1]
@@ -1676,7 +1682,8 @@ export const parseGradleProjects = function (rawOutput) {
1676
1682
  const projName = tmpB[1].split(" ")[0].replace(/'/g, "");
1677
1683
  // Include all projects including test projects
1678
1684
  if (projName.startsWith(":")) {
1679
- projects.add(projName);
1685
+ // Handle the case where the project name could have a space. Eg: +--- project :app (*)
1686
+ projects.add(projName.split(" ")[0]);
1680
1687
  }
1681
1688
  }
1682
1689
  } else if (l.includes("--- project ")) {
@@ -1684,7 +1691,7 @@ export const parseGradleProjects = function (rawOutput) {
1684
1691
  if (tmpB && tmpB.length > 1) {
1685
1692
  const projName = tmpB[1];
1686
1693
  if (projName.startsWith(":")) {
1687
- projects.add(projName);
1694
+ projects.add(projName.split(" ")[0]);
1688
1695
  }
1689
1696
  }
1690
1697
  }
@@ -1726,7 +1733,7 @@ export const parseGradleProperties = function (rawOutput) {
1726
1733
  const spStrs = tmpB[1].replace(/[[\]']/g, "").split(", ");
1727
1734
  const tmpprojects = spStrs
1728
1735
  .flatMap((s) => s.replace("project ", ""))
1729
- .filter((s) => ![":app", ""].includes(s.trim()));
1736
+ .filter((s) => ![""].includes(s.trim()));
1730
1737
  tmpprojects.forEach(projects.add, projects);
1731
1738
  }
1732
1739
  }
@@ -1893,6 +1900,7 @@ export const parseKVDep = function (rawOutput) {
1893
1900
  if (typeof rawOutput === "string") {
1894
1901
  const deps = [];
1895
1902
  rawOutput.split("\n").forEach((l) => {
1903
+ l = l.replace("\r", "");
1896
1904
  const tmpA = l.split(":");
1897
1905
  if (tmpA.length === 3) {
1898
1906
  deps.push({
@@ -2331,6 +2339,9 @@ export const parsePyProjectToml = (tomlFile) => {
2331
2339
  pkg.name = value;
2332
2340
  break;
2333
2341
  case "version":
2342
+ if (value.includes("{")) {
2343
+ value = "latest";
2344
+ }
2334
2345
  pkg.version = value;
2335
2346
  break;
2336
2347
  case "authors":
@@ -2365,6 +2376,7 @@ export const parsePoetrylockData = async function (lockData, lockFile) {
2365
2376
  return pkgList;
2366
2377
  }
2367
2378
  lockData.split("\n").forEach((l) => {
2379
+ l = l.replace("\r", "");
2368
2380
  let key = null;
2369
2381
  let value = null;
2370
2382
  // Package section starts with this marker
@@ -2420,6 +2432,7 @@ export async function parseReqFile(reqData, fetchDepsInfo) {
2420
2432
  const pkgList = [];
2421
2433
  let compScope = undefined;
2422
2434
  reqData.split("\n").forEach((l) => {
2435
+ l = l.replace("\r", "");
2423
2436
  l = l.trim();
2424
2437
  if (l.startsWith("Skipping line") || l.startsWith("(add")) {
2425
2438
  return;
@@ -3146,6 +3159,7 @@ export const parseGemfileLockData = async function (gemLockData) {
3146
3159
  let specsFound = false;
3147
3160
  gemLockData.split("\n").forEach((l) => {
3148
3161
  l = l.trim();
3162
+ l = l.replace("\r", "");
3149
3163
  if (specsFound) {
3150
3164
  const tmpA = l.split(" ");
3151
3165
  if (tmpA && tmpA.length == 2) {
@@ -3358,6 +3372,7 @@ export const parseCargoData = async function (cargoData) {
3358
3372
  cargoData.split("\n").forEach((l) => {
3359
3373
  let key = null;
3360
3374
  let value = null;
3375
+ l = l.replace("\r", "");
3361
3376
  // Ignore version = 3 found at the top of newer lock files
3362
3377
  if (!pkg && l.startsWith("version =")) {
3363
3378
  return;
@@ -3402,6 +3417,7 @@ export const parseCargoAuditableData = async function (cargoData) {
3402
3417
  return pkgList;
3403
3418
  }
3404
3419
  cargoData.split("\n").forEach((l) => {
3420
+ l = l.replace("\r", "");
3405
3421
  const tmpA = l.split("\t");
3406
3422
  if (tmpA && tmpA.length > 2) {
3407
3423
  let group = dirname(tmpA[0].trim());
@@ -3433,6 +3449,7 @@ export const parsePubLockData = async function (pubLockData) {
3433
3449
  pubLockData.split("\n").forEach((l) => {
3434
3450
  let key = null;
3435
3451
  let value = null;
3452
+ l = l.replace("\r", "");
3436
3453
  if (!pkg && (l.startsWith("sdks:") || !l.startsWith(" "))) {
3437
3454
  return;
3438
3455
  }
@@ -3848,6 +3865,7 @@ export const parseCabalData = function (cabalData) {
3848
3865
  if (!l.includes(" ==")) {
3849
3866
  return;
3850
3867
  }
3868
+ l = l.replace("\r", "");
3851
3869
  if (l.includes(" ==")) {
3852
3870
  const tmpA = l.split(" ==");
3853
3871
  const name = tmpA[0]
@@ -3875,6 +3893,7 @@ export const parseMixLockData = function (mixData) {
3875
3893
  if (!l.includes(":hex")) {
3876
3894
  return;
3877
3895
  }
3896
+ l = l.replace("\r", "");
3878
3897
  if (l.includes(":hex")) {
3879
3898
  const tmpA = l.split(",");
3880
3899
  if (tmpA.length > 3) {
@@ -4969,6 +4988,7 @@ export const parseJarManifest = function (jarMetadata) {
4969
4988
  return metadata;
4970
4989
  }
4971
4990
  jarMetadata.split("\n").forEach((l) => {
4991
+ l = l.replace("\r", "");
4972
4992
  if (l.includes(": ")) {
4973
4993
  const tmpA = l.split(": ");
4974
4994
  if (tmpA && tmpA.length === 2) {
@@ -4999,10 +5019,19 @@ export const extractJarArchive = function (jarFile, tempDir) {
4999
5019
  const fname = basename(jarFile);
5000
5020
  let pomname = undefined;
5001
5021
  // If there is a pom file in the same directory, try to use it
5022
+ let manifestname = join(dirname(jarFile), "META-INF", "MANIFEST.MF");
5023
+ // Issue 439: Current implementation checks for existance of a .pom file, but .pom file is not used.
5024
+ // Instead code expects to find META-INF/MANIFEST.MF in the same folder as a .jar file.
5025
+ // For now check for presence of both .pom and MANIFEST.MF files.
5002
5026
  if (jarFile.endsWith(".jar")) {
5003
5027
  pomname = jarFile.replace(".jar", ".pom");
5004
5028
  }
5005
- if (pomname && existsSync(pomname)) {
5029
+ if (
5030
+ pomname &&
5031
+ existsSync(pomname) &&
5032
+ manifestname &&
5033
+ existsSync(manifestname)
5034
+ ) {
5006
5035
  tempDir = dirname(jarFile);
5007
5036
  } else if (!existsSync(join(tempDir, fname))) {
5008
5037
  // Only copy if the file doesn't exist
@@ -5548,7 +5577,11 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5548
5577
  *
5549
5578
  * By checking the environment variable "VIRTUAL_ENV" we decide whether to create an env or not
5550
5579
  */
5551
- if (!process.env.VIRTUAL_ENV) {
5580
+ if (
5581
+ !process.env.VIRTUAL_ENV &&
5582
+ reqOrSetupFile &&
5583
+ !reqOrSetupFile.endsWith("poetry.lock")
5584
+ ) {
5552
5585
  result = spawnSync(PYTHON_CMD, ["-m", "venv", tempVenvDir], {
5553
5586
  encoding: "utf-8"
5554
5587
  });
@@ -5562,29 +5595,43 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5562
5595
  }
5563
5596
  }
5564
5597
  } else {
5598
+ if (DEBUG_MODE) {
5599
+ console.log(join("Using virtual environment in ", tempVenvDir));
5600
+ }
5565
5601
  env.VIRTUAL_ENV = tempVenvDir;
5566
5602
  env.PATH = `${join(
5567
5603
  tempVenvDir,
5568
- platform() == "win32" ? "Scripts" : "bin"
5604
+ platform() === "win32" ? "Scripts" : "bin"
5569
5605
  )}${_delimiter}${process.env.PATH || ""}`;
5570
5606
  }
5571
5607
  }
5572
5608
  /**
5573
5609
  * We now have a virtual environment so we can attempt to install the project and perform
5574
5610
  * pip freeze to collect the packages that got installed.
5611
+ * Note that we did not create a virtual environment for poetry because poetry will do this when we run the install.
5575
5612
  * This step is accurate but not reproducible since the resulting list could differ based on various factors
5576
5613
  * such as the version of python, pip, os, pypi.org availability (and weather?)
5577
5614
  */
5578
5615
  // Bug #388. Perform pip install in all virtualenv to make the experience consistent
5579
5616
  if (reqOrSetupFile) {
5580
5617
  if (reqOrSetupFile.endsWith("poetry.lock")) {
5581
- let poetryInstallArgs = ["-m", "poetry", "install", "-n", "--no-root"];
5618
+ let poetryConfigArgs = [
5619
+ "config",
5620
+ "virtualenvs.options.no-setuptools",
5621
+ "true",
5622
+ "--local"
5623
+ ];
5624
+ result = spawnSync("poetry", poetryConfigArgs, {
5625
+ cwd: basePath,
5626
+ encoding: "utf-8",
5627
+ timeout: TIMEOUT_MS
5628
+ });
5629
+ let poetryInstallArgs = ["install", "-n", "--no-root"];
5582
5630
  // Attempt to perform poetry install
5583
- result = spawnSync(PYTHON_CMD, poetryInstallArgs, {
5631
+ result = spawnSync("poetry", poetryInstallArgs, {
5584
5632
  cwd: basePath,
5585
5633
  encoding: "utf-8",
5586
- timeout: TIMEOUT_MS,
5587
- env
5634
+ timeout: TIMEOUT_MS
5588
5635
  });
5589
5636
  if (result.status !== 0 || result.error) {
5590
5637
  if (result.stderr && result.stderr.includes("No module named poetry")) {
@@ -5597,6 +5644,9 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5597
5644
  env
5598
5645
  });
5599
5646
  if (result.status !== 0 || result.error) {
5647
+ if (DEBUG_MODE && result.stderr) {
5648
+ console.log(result.stderr);
5649
+ }
5600
5650
  console.log("poetry install has failed.");
5601
5651
  console.log(
5602
5652
  "1. Install the poetry command using python -m pip install poetry."
@@ -5610,11 +5660,27 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5610
5660
  }
5611
5661
  } else {
5612
5662
  console.log(
5613
- "poetry install has failed. Setup and activate the poetry virtual environment and re-run cdxgen."
5663
+ "Poetry install has failed. Setup and activate the poetry virtual environment and re-run cdxgen."
5614
5664
  );
5615
5665
  }
5666
+ } else {
5667
+ let poetryEnvArgs = ["env info", "--path"];
5668
+ result = spawnSync("poetry", poetryEnvArgs, {
5669
+ cwd: basePath,
5670
+ encoding: "utf-8",
5671
+ timeout: TIMEOUT_MS,
5672
+ env
5673
+ });
5674
+ tempVenvDir = result.stdout.replaceAll(/[\r\n]+/g, "");
5675
+ if (tempVenvDir && tempVenvDir.length) {
5676
+ env.VIRTUAL_ENV = tempVenvDir;
5677
+ env.PATH = `${join(
5678
+ tempVenvDir,
5679
+ platform() === "win32" ? "Scripts" : "bin"
5680
+ )}${_delimiter}${process.env.PATH || ""}`;
5681
+ }
5616
5682
  }
5617
- } else if (!reqOrSetupFile.endsWith(".lock")) {
5683
+ } else {
5618
5684
  const pipInstallArgs = [
5619
5685
  "-m",
5620
5686
  "pip",
@@ -5659,7 +5725,7 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5659
5725
  console.log(
5660
5726
  "Possible build errors detected. The resulting list in the SBoM would therefore be incomplete.\nTry installing any missing build tools or development libraries to improve the accuracy."
5661
5727
  );
5662
- if (platform() == "win32") {
5728
+ if (platform() === "win32") {
5663
5729
  console.log(
5664
5730
  "- Install the appropriate compilers and build tools on Windows by following this documentation - https://wiki.python.org/moin/WindowsCompilers"
5665
5731
  );
@@ -5703,28 +5769,32 @@ export const getPipFrozenTree = (basePath, reqOrSetupFile, tempVenvDir) => {
5703
5769
  continue;
5704
5770
  }
5705
5771
  const name = t.name.replace(/_/g, "-").toLowerCase();
5706
- pkgList.push({
5707
- name,
5708
- version: t.version,
5709
- evidence: {
5710
- identity: {
5711
- field: "purl",
5712
- confidence: 1,
5713
- methods: [
5714
- {
5715
- technique: "instrumentation",
5716
- confidence: 1,
5717
- value: env.VIRTUAL_ENV
5718
- }
5719
- ]
5772
+ const version = t.version;
5773
+ let exclude = ["pip", "setuptools", "wheel"];
5774
+ if (!exclude.includes(name)) {
5775
+ pkgList.push({
5776
+ name,
5777
+ version,
5778
+ evidence: {
5779
+ identity: {
5780
+ field: "purl",
5781
+ confidence: 1,
5782
+ methods: [
5783
+ {
5784
+ technique: "instrumentation",
5785
+ confidence: 1,
5786
+ value: env.VIRTUAL_ENV
5787
+ }
5788
+ ]
5789
+ }
5720
5790
  }
5721
- }
5722
- });
5723
- rootList.push({
5724
- name,
5725
- version: t.version
5726
- });
5727
- flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t);
5791
+ });
5792
+ rootList.push({
5793
+ name,
5794
+ version
5795
+ });
5796
+ flattenDeps(dependenciesMap, pkgList, reqOrSetupFile, t);
5797
+ }
5728
5798
  } // end for
5729
5799
  for (const k of Object.keys(dependenciesMap)) {
5730
5800
  dependenciesList.push({ ref: k, dependsOn: dependenciesMap[k] });
package/utils.test.js CHANGED
@@ -295,6 +295,10 @@ test("parse gradle dependencies", () => {
295
295
  );
296
296
  expect(parsedList.pkgList.length).toEqual(152);
297
297
  expect(parsedList.dependenciesList.length).toEqual(153);
298
+ parsedList = parseGradleDep(
299
+ readFileSync("./test/data/gradle-android-app.dep", { encoding: "utf-8" })
300
+ );
301
+ expect(parsedList.pkgList.length).toEqual(101);
298
302
  });
299
303
 
300
304
  test("parse gradle projects", () => {
@@ -317,6 +321,11 @@ test("parse gradle projects", () => {
317
321
  );
318
322
  expect(retMap.rootProject).toEqual("fineract");
319
323
  expect(retMap.projects.length).toEqual(22);
324
+ retMap = parseGradleProjects(
325
+ readFileSync("./test/data/gradle-android-app.dep", { encoding: "utf-8" })
326
+ );
327
+ expect(retMap.rootProject).toEqual("root");
328
+ expect(retMap.projects).toEqual([":app"]);
320
329
  });
321
330
 
322
331
  test("parse gradle properties", () => {
@@ -366,7 +375,7 @@ test("parse gradle properties", () => {
366
375
  );
367
376
  expect(retMap).toEqual({
368
377
  rootProject: "java-test",
369
- projects: [],
378
+ projects: [":app"],
370
379
  metadata: {
371
380
  group: "com.ajmalab.demo",
372
381
  version: "latest",
@@ -411,6 +420,13 @@ test("parse gradle properties", () => {
411
420
  );
412
421
  expect(retMap.rootProject).toEqual("elasticsearch");
413
422
  expect(retMap.projects.length).toEqual(409);
423
+ retMap = parseGradleProperties(
424
+ readFileSync("./test/data/gradle-properties-android.txt", {
425
+ encoding: "utf-8"
426
+ })
427
+ );
428
+ expect(retMap.rootProject).toEqual("CdxgenAndroidTest");
429
+ expect(retMap.projects.length).toEqual(2);
414
430
  });
415
431
 
416
432
  test("parse maven tree", () => {
@@ -1290,16 +1306,19 @@ test("parsePkgLock", async () => {
1290
1306
  version: "2.0.0"
1291
1307
  });
1292
1308
  expect(deps[deps.length - 1].name).toEqual("zone.js");
1293
- parsedList = await parsePkgLock("./test/data/package-lock-v3.json");
1309
+ parsedList = await parsePkgLock("./test/data/package-lock-v3.json", {
1310
+ projectVersion: "latest",
1311
+ projectName: "cdxgen"
1312
+ });
1294
1313
  deps = parsedList.pkgList;
1295
1314
  expect(deps.length).toEqual(879);
1296
1315
  expect(parsedList.dependenciesList.length).toEqual(879);
1297
1316
  expect(deps[0]).toEqual({
1298
- "bom-ref": "pkg:npm/@cyclonedx/cdxgen@8.4.3",
1317
+ "bom-ref": "pkg:npm/cdxgen@latest",
1299
1318
  group: "",
1300
- name: "@cyclonedx/cdxgen",
1319
+ name: "cdxgen",
1301
1320
  type: "application",
1302
- version: "8.4.3"
1321
+ version: "latest"
1303
1322
  });
1304
1323
  expect(deps[deps.length - 1].name).toEqual("yocto-queue");
1305
1324
  parsedList = await parsePkgLock("./test/data/package-lock4.json");