@cyclonedx/cdxgen 9.8.8 → 9.8.10

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
@@ -126,6 +126,7 @@ import { createBom, submitBom } from "npm:@cyclonedx/cdxgen@^9.0.1";
126
126
 
127
127
  ```text
128
128
  $ cdxgen -h
129
+ Options:
129
130
  -o, --output Output file for bom.xml or bom.json. Default bom.
130
131
  json
131
132
  -t, --type Project type
@@ -149,7 +150,9 @@ $ cdxgen -h
149
150
  d or the project name and version together
150
151
  --parent-project-id Dependency track parent project id
151
152
  --required-only Include only the packages with required scope on
152
- the SBOM. [boolean]
153
+ the SBOM. Would set compositions.aggregate to inc
154
+ omplete unless --no-auto-compositions is passed.
155
+ [boolean]
153
156
  --fail-on-error Fail if any dependency extractor fails. [boolean]
154
157
  --no-babel Do not use babel to perform usage analysis for Ja
155
158
  vaScript/TypeScript projects. [boolean]
@@ -166,11 +169,21 @@ $ cdxgen -h
166
169
  --validate Validate the generated SBOM using json schema. De
167
170
  faults to true. Pass --no-validate to disable.
168
171
  [boolean] [default: true]
172
+ --evidence Generate SBOM with evidence for supported languag
173
+ es. WIP [boolean] [default: false]
169
174
  --usages-slices-file Path for the usages slice file created by atom.
170
175
  --data-flow-slices-file Path for the data-flow slice file created by atom
171
176
  .
172
177
  --spec-version CycloneDX Specification version to use. Defaults
173
178
  to 1.5 [default: 1.5]
179
+ --filter Filter components containining this word in purl.
180
+ Multiple values allowed. [array]
181
+ --only Include components only containining this word in
182
+ purl. Useful to generate BOM with first party co
183
+ mponents alone. Multiple values allowed. [array]
184
+ --auto-compositions Automatically set compositions when the BOM was f
185
+ iltered. Defaults to true
186
+ [boolean] [default: true]
174
187
  -h, --help Show help [boolean]
175
188
  -v, --version Show version number [boolean]
176
189
  ```
package/bin/cdxgen.js CHANGED
@@ -11,6 +11,29 @@ import { fileURLToPath } from "node:url";
11
11
  import globalAgent from "global-agent";
12
12
  import process from "node:process";
13
13
  import { printTable, printDependencyTree } from "../display.js";
14
+ import { findUpSync } from "find-up";
15
+ import { load as _load } from "js-yaml";
16
+ import { postProcess } from "../postgen.js";
17
+
18
+ // Support for config files
19
+ const configPath = findUpSync([
20
+ ".cdxgenrc",
21
+ ".cdxgen.json",
22
+ ".cdxgen.yml",
23
+ ".cdxgen.yaml"
24
+ ]);
25
+ let config = {};
26
+ if (configPath) {
27
+ try {
28
+ if (configPath.endsWith(".yml") || configPath.endsWith(".yaml")) {
29
+ config = _load(fs.readFileSync(configPath, "utf-8"));
30
+ } else {
31
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
32
+ }
33
+ } catch (e) {
34
+ console.log("Invalid config file", configPath);
35
+ }
36
+ }
14
37
 
15
38
  let url = import.meta.url;
16
39
  if (!url.startsWith("file://")) {
@@ -22,6 +45,7 @@ import yargs from "yargs";
22
45
  import { hideBin } from "yargs/helpers";
23
46
 
24
47
  const args = yargs(hideBin(process.argv))
48
+ .env("CDXGEN")
25
49
  .option("output", {
26
50
  alias: "o",
27
51
  description: "Output file for bom.xml or bom.json. Default bom.json"
@@ -77,7 +101,8 @@ const args = yargs(hideBin(process.argv))
77
101
  })
78
102
  .option("required-only", {
79
103
  type: "boolean",
80
- description: "Include only the packages with required scope on the SBOM."
104
+ description:
105
+ "Include only the packages with required scope on the SBOM. Would set compositions.aggregate to incomplete unless --no-auto-compositions is passed."
81
106
  })
82
107
  .option("fail-on-error", {
83
108
  type: "boolean",
@@ -132,6 +157,28 @@ const args = yargs(hideBin(process.argv))
132
157
  description: "CycloneDX Specification version to use. Defaults to 1.5",
133
158
  default: 1.5
134
159
  })
160
+ .option("filter", {
161
+ description:
162
+ "Filter components containining this word in purl. Multiple values allowed."
163
+ })
164
+ .option("only", {
165
+ description:
166
+ "Include components only containining this word in purl. Useful to generate BOM with first party components alone. Multiple values allowed."
167
+ })
168
+ .array("filter")
169
+ .array("only")
170
+ .option("auto-compositions", {
171
+ type: "boolean",
172
+ default: true,
173
+ description:
174
+ "Automatically set compositions when the BOM was filtered. Defaults to true"
175
+ })
176
+ .example([
177
+ ["$0 -t java .", "Generate a Java SBOM for the current directory"],
178
+ ["$0 --server", "Run cdxgen as a server"]
179
+ ])
180
+ .epilogue("for documentation, visit https://cyclonedx.github.io/cdxgen")
181
+ .config(config)
135
182
  .scriptName("cdxgen")
136
183
  .version()
137
184
  .alias("v", "version")
@@ -177,32 +224,14 @@ if (process.argv[1].includes("obom") && !args.type) {
177
224
  }
178
225
 
179
226
  /**
180
- * projectType: python, nodejs, java, golang
181
- * multiProject: Boolean to indicate monorepo or multi-module projects
227
+ * Command line options
182
228
  */
183
- const options = {
229
+ const options = Object.assign({}, args, {
184
230
  projectType: args.type,
185
231
  multiProject: args.recurse,
186
- output: args.output,
187
- resolveClass: args.resolveClass,
188
- installDeps: args.installDeps,
189
- requiredOnly: args.requiredOnly,
190
- failOnError: args.failOnError,
191
232
  noBabel: args.noBabel || args.babel === false,
192
- deep: args.deep,
193
- generateKeyAndSign: args.generateKeyAndSign,
194
- project: args.projectId,
195
- projectName: args.projectName,
196
- projectGroup: args.projectGroup,
197
- projectVersion: args.projectVersion,
198
- server: args.server,
199
- serverHost: args.serverHost,
200
- serverPort: args.serverPort,
201
- specVersion: args.specVersion,
202
- evidence: args.evidence,
203
- usagesSlicesFile: args.usagesSlicesFile,
204
- dataFlowSlicesFile: args.dataFlowSlicesFile
205
- };
233
+ project: args.projectId
234
+ });
206
235
 
207
236
  /**
208
237
  * Check for node >= 20 permissions
@@ -243,7 +272,7 @@ const checkPermissions = (filePath) => {
243
272
  // Start SBOM server
244
273
  if (args.server) {
245
274
  const serverModule = await import("../server.js");
246
- return await serverModule.start(options);
275
+ return serverModule.start(options);
247
276
  }
248
277
  // Check if cdxgen has the required permissions
249
278
  if (!checkPermissions(filePath)) {
@@ -253,7 +282,10 @@ const checkPermissions = (filePath) => {
253
282
  if (!options.usagesSlicesFile) {
254
283
  options.usagesSlicesFile = `${options.projectName}-usages.json`;
255
284
  }
256
- const bomNSData = (await createBom(filePath, options)) || {};
285
+ let bomNSData = (await createBom(filePath, options)) || {};
286
+ if (options.requiredOnly || options["filter"] || options["only"]) {
287
+ bomNSData = postProcess(bomNSData, options);
288
+ }
257
289
  if (!args.output) {
258
290
  args.output = "bom.json";
259
291
  }
package/bin/evinse.js CHANGED
@@ -10,6 +10,28 @@ import process from "node:process";
10
10
  import { analyzeProject, createEvinseFile, prepareDB } from "../evinser.js";
11
11
  import { validateBom } from "../validator.js";
12
12
  import { printCallStack, printOccurrences, printServices } from "../display.js";
13
+ import { findUpSync } from "find-up";
14
+ import { load as _load } from "js-yaml";
15
+
16
+ // Support for config files
17
+ const configPath = findUpSync([
18
+ ".cdxgenrc",
19
+ ".cdxgen.json",
20
+ ".cdxgen.yml",
21
+ ".cdxgen.yaml"
22
+ ]);
23
+ let config = {};
24
+ if (configPath) {
25
+ try {
26
+ if (configPath.endsWith(".yml") || configPath.endsWith(".yaml")) {
27
+ config = _load(fs.readFileSync(configPath, "utf-8"));
28
+ } else {
29
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
30
+ }
31
+ } catch (e) {
32
+ console.log("Invalid config file", configPath);
33
+ }
34
+ }
13
35
 
14
36
  const isWin = _platform() === "win32";
15
37
  const isMac = _platform() === "darwin";
@@ -28,6 +50,7 @@ if (!process.env.ATOM_DB && !fs.existsSync(ATOM_DB)) {
28
50
  }
29
51
  }
30
52
  const args = yargs(hideBin(process.argv))
53
+ .env("EVINSE")
31
54
  .option("input", {
32
55
  alias: "i",
33
56
  description: "Input SBOM file. Default bom.json",
@@ -88,6 +111,7 @@ const args = yargs(hideBin(process.argv))
88
111
  type: "boolean",
89
112
  description: "Print the evidences as table"
90
113
  })
114
+ .config(config)
91
115
  .scriptName("evinse")
92
116
  .version()
93
117
  .help("h").argv;
package/index.js CHANGED
@@ -707,9 +707,6 @@ function addComponent(
707
707
  compScope = "optional";
708
708
  }
709
709
  }
710
- if (options.requiredOnly && ["optional", "excluded"].includes(compScope)) {
711
- return;
712
- }
713
710
  const component = {
714
711
  author,
715
712
  publisher,
@@ -1016,7 +1013,7 @@ const buildBomNSData = (options, pkgInfo, ptype, context) => {
1016
1013
  allImports = context.allImports;
1017
1014
  }
1018
1015
  const nsMapping = context.nsMapping || {};
1019
- const dependencies = !options.requiredOnly ? context.dependencies || [] : [];
1016
+ const dependencies = context.dependencies || [];
1020
1017
  const parentComponent =
1021
1018
  determineParentComponent(options) || context.parentComponent;
1022
1019
  const metadata = addMetadata(parentComponent, "json", options);
@@ -1330,7 +1327,7 @@ export const createJavaBom = async (path, options) => {
1330
1327
  if (bomJsonObj.components) {
1331
1328
  pkgList = pkgList.concat(bomJsonObj.components);
1332
1329
  }
1333
- if (bomJsonObj.dependencies && !options.requiredOnly) {
1330
+ if (bomJsonObj.dependencies) {
1334
1331
  dependencies = mergeDependencies(
1335
1332
  dependencies,
1336
1333
  bomJsonObj.dependencies,
@@ -2697,7 +2694,7 @@ export const createGoBom = async (path, options) => {
2697
2694
  const dlist = await parseGosumData(gosumData);
2698
2695
  if (dlist && dlist.length) {
2699
2696
  dlist.forEach((pkg) => {
2700
- gosumMap[`${pkg.group}/${pkg.name}@${pkg.version}`] = pkg._integrity;
2697
+ gosumMap[`${pkg.name}@${pkg.version}`] = pkg._integrity;
2701
2698
  });
2702
2699
  }
2703
2700
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyclonedx/cdxgen",
3
- "version": "9.8.8",
3
+ "version": "9.8.10",
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>",
@@ -39,10 +39,10 @@
39
39
  },
40
40
  "scripts": {
41
41
  "docs": "docsify serve docs",
42
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --inject-globals false docker.test.js utils.test.js display.test.js",
42
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --inject-globals false docker.test.js utils.test.js display.test.js postgen.test.js",
43
43
  "watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --inject-globals false",
44
44
  "lint": "eslint *.js *.test.js bin/*.js",
45
- "pretty": "prettier --write *.js data/*.json bin/*.js"
45
+ "pretty": "prettier --write *.js data/*.json bin/*.js *.md docs/*.md"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=16"
@@ -57,11 +57,12 @@
57
57
  "dependencies": {
58
58
  "@babel/parser": "^7.23.0",
59
59
  "@babel/traverse": "^7.23.0",
60
- "@npmcli/arborist": "^7.1.0",
60
+ "@npmcli/arborist": "^7.2.0",
61
61
  "ajv": "^8.12.0",
62
62
  "ajv-formats": "^2.1.1",
63
63
  "cheerio": "^1.0.0-rc.12",
64
64
  "edn-data": "^1.0.0",
65
+ "find-up": "^6.3.0",
65
66
  "glob": "^10.3.10",
66
67
  "global-agent": "^3.0.0",
67
68
  "got": "^13.0.0",
@@ -101,9 +102,9 @@
101
102
  "devDependencies": {
102
103
  "caxa": "^3.0.1",
103
104
  "docsify-cli": "^4.4.4",
104
- "eslint": "^8.50.0",
105
+ "eslint": "^8.51.0",
105
106
  "eslint-config-prettier": "^9.0.0",
106
- "eslint-plugin-prettier": "^5.0.0",
107
+ "eslint-plugin-prettier": "^5.0.1",
107
108
  "jest": "^29.7.0",
108
109
  "prettier": "3.0.3"
109
110
  }
package/postgen.js ADDED
@@ -0,0 +1,92 @@
1
+ export const postProcess = (bomNSData, options) => {
2
+ let jsonPayload = bomNSData.bomJson;
3
+ if (
4
+ typeof bomNSData.bomJson === "string" ||
5
+ bomNSData.bomJson instanceof String
6
+ ) {
7
+ jsonPayload = JSON.parse(bomNSData.bomJson);
8
+ }
9
+ bomNSData.bomJson = filterBom(jsonPayload, options);
10
+ return bomNSData;
11
+ };
12
+
13
+ export const filterBom = (bomJson, options) => {
14
+ const newPkgMap = {};
15
+ let filtered = false;
16
+ for (const comp of bomJson.components) {
17
+ if (
18
+ options.requiredOnly &&
19
+ comp.scope &&
20
+ ["optional", "excluded"].includes(comp.scope)
21
+ ) {
22
+ filtered = true;
23
+ continue;
24
+ } else if (options.only && options.only.length) {
25
+ if (!Array.isArray(options.only)) {
26
+ options.only = [options.only];
27
+ }
28
+ let purlfiltered = false;
29
+ for (const filterstr of options.only) {
30
+ if (filterstr.length && !comp.purl.toLowerCase().includes(filterstr)) {
31
+ filtered = true;
32
+ purlfiltered = true;
33
+ continue;
34
+ }
35
+ }
36
+ if (!purlfiltered) {
37
+ newPkgMap[comp["bom-ref"]] = comp;
38
+ }
39
+ } else if (options.filter && options.filter.length) {
40
+ if (!Array.isArray(options.filter)) {
41
+ options.filter = [options.filter];
42
+ }
43
+ let purlfiltered = false;
44
+ for (const filterstr of options.filter) {
45
+ if (filterstr.length && comp.purl.toLowerCase().includes(filterstr)) {
46
+ filtered = true;
47
+ purlfiltered = true;
48
+ continue;
49
+ }
50
+ }
51
+ if (!purlfiltered) {
52
+ newPkgMap[comp["bom-ref"]] = comp;
53
+ }
54
+ } else {
55
+ newPkgMap[comp["bom-ref"]] = comp;
56
+ }
57
+ }
58
+ if (filtered) {
59
+ const newcomponents = [];
60
+ const newdependencies = [];
61
+ for (const aref of Object.keys(newPkgMap).sort()) {
62
+ newcomponents.push(newPkgMap[aref]);
63
+ }
64
+ for (const adep of bomJson.dependencies) {
65
+ if (newPkgMap[adep.ref]) {
66
+ const newdepson = (adep.dependsOn || []).filter((d) => newPkgMap[d]);
67
+ newdependencies.push({
68
+ ref: adep.ref,
69
+ dependsOn: newdepson
70
+ });
71
+ }
72
+ }
73
+ bomJson.components = newcomponents;
74
+ bomJson.dependencies = newdependencies;
75
+ // We set the compositions.aggregate to incomplete by default
76
+ if (
77
+ options.specVersion >= 1.5 &&
78
+ options.autoCompositions &&
79
+ bomJson.metadata &&
80
+ bomJson.metadata.component
81
+ ) {
82
+ if (!bomJson.compositions) {
83
+ bomJson.compositions = [];
84
+ }
85
+ bomJson.compositions.push({
86
+ "bom-ref": bomJson.metadata.component["bom-ref"],
87
+ aggregate: options.only ? "incomplete_first_party_only" : "incomplete"
88
+ });
89
+ }
90
+ }
91
+ return bomJson;
92
+ };
@@ -0,0 +1,70 @@
1
+ import { filterBom } from "./postgen.js";
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { expect, test } from "@jest/globals";
5
+
6
+ test("filter bom tests", () => {
7
+ const bomJson = JSON.parse(
8
+ readFileSync("./test/data/bom-postgen-test.json", "utf-8")
9
+ );
10
+ let newBom = filterBom(bomJson, {});
11
+ expect(bomJson).toEqual(newBom);
12
+ expect(newBom.components.length).toEqual(1060);
13
+ newBom = filterBom(bomJson, { requiredOnly: true });
14
+ for (const comp of newBom.components) {
15
+ if (comp.scope && comp.scope !== "required") {
16
+ throw new Error(`${comp.scope} is unexpected`);
17
+ }
18
+ }
19
+ expect(newBom.components.length).toEqual(345);
20
+ });
21
+
22
+ test("filter bom tests2", () => {
23
+ const bomJson = JSON.parse(
24
+ readFileSync("./test/data/bom-postgen-test2.json", "utf-8")
25
+ );
26
+ let newBom = filterBom(bomJson, {});
27
+ expect(bomJson).toEqual(newBom);
28
+ expect(newBom.components.length).toEqual(199);
29
+ newBom = filterBom(bomJson, { requiredOnly: true });
30
+ for (const comp of newBom.components) {
31
+ if (comp.scope && comp.scope !== "required") {
32
+ throw new Error(`${comp.scope} is unexpected`);
33
+ }
34
+ }
35
+ expect(newBom.components.length).toEqual(199);
36
+ newBom = filterBom(bomJson, { filter: [""] });
37
+ expect(newBom.components.length).toEqual(199);
38
+ newBom = filterBom(bomJson, { filter: ["apache"] });
39
+ for (const comp of newBom.components) {
40
+ if (comp.purl.includes("apache")) {
41
+ throw new Error(`${comp.purl} is unexpected`);
42
+ }
43
+ }
44
+ expect(newBom.components.length).toEqual(177);
45
+ newBom = filterBom(bomJson, { filter: ["apache", "json"] });
46
+ for (const comp of newBom.components) {
47
+ if (comp.purl.includes("apache") || comp.purl.includes("json")) {
48
+ throw new Error(`${comp.purl} is unexpected`);
49
+ }
50
+ }
51
+ expect(newBom.components.length).toEqual(172);
52
+ expect(newBom.compositions).toBeUndefined();
53
+ newBom = filterBom(bomJson, {
54
+ only: ["org.springframework"],
55
+ specVersion: 1.5,
56
+ autoCompositions: true
57
+ });
58
+ for (const comp of newBom.components) {
59
+ if (!comp.purl.includes("org.springframework")) {
60
+ throw new Error(`${comp.purl} is unexpected`);
61
+ }
62
+ }
63
+ expect(newBom.components.length).toEqual(37);
64
+ expect(newBom.compositions).toEqual([
65
+ {
66
+ aggregate: "incomplete_first_party_only",
67
+ "bom-ref": "pkg:maven/sec/java-sec-code@1.0.0?type=jar"
68
+ }
69
+ ]);
70
+ });
package/server.js CHANGED
@@ -7,6 +7,8 @@ import os from "node:os";
7
7
  import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import { createBom, submitBom } from "./index.js";
10
+ import { postProcess } from "./postgen.js";
11
+
10
12
  import compression from "compression";
11
13
 
12
14
  // Timeout milliseconds. Default 10 mins
@@ -60,7 +62,10 @@ const parseQueryString = (q, body, options = {}) => {
60
62
  "parentUUID",
61
63
  "serverUrl",
62
64
  "apiKey",
63
- "specVersion"
65
+ "specVersion",
66
+ "filter",
67
+ "only",
68
+ "autoCompositions"
64
69
  ];
65
70
 
66
71
  for (const param of queryParams) {
@@ -94,7 +99,7 @@ const start = (options) => {
94
99
  .listen(options.serverPort, options.serverHost);
95
100
  configureServer(cdxgenServer);
96
101
 
97
- app.use("/health", async function (req, res) {
102
+ app.use("/health", async function (_req, res) {
98
103
  res.setHeader("Content-Type", "application/json");
99
104
  res.end(JSON.stringify({ status: "OK" }, null, 2));
100
105
  });
@@ -102,7 +107,11 @@ const start = (options) => {
102
107
  app.use("/sbom", async function (req, res) {
103
108
  const q = url.parse(req.url, true).query;
104
109
  let cleanup = false;
105
- options = parseQueryString(q, req.body, options);
110
+ const reqOptions = parseQueryString(
111
+ q,
112
+ req.body,
113
+ Object.assign({}, options)
114
+ );
106
115
  const filePath = q.path || q.url || req.body.path || req.body.url;
107
116
  if (!filePath) {
108
117
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -117,7 +126,10 @@ const start = (options) => {
117
126
  cleanup = true;
118
127
  }
119
128
  console.log("Generating SBOM for", srcDir);
120
- const bomNSData = (await createBom(srcDir, options)) || {};
129
+ let bomNSData = (await createBom(srcDir, reqOptions)) || {};
130
+ if (reqOptions.requiredOnly || reqOptions["filter"] || reqOptions["only"]) {
131
+ bomNSData = postProcess(bomNSData, reqOptions);
132
+ }
121
133
  if (bomNSData.bomJson) {
122
134
  if (
123
135
  typeof bomNSData.bomJson === "string" ||
@@ -128,9 +140,9 @@ const start = (options) => {
128
140
  res.write(JSON.stringify(bomNSData.bomJson, null, 2));
129
141
  }
130
142
  }
131
- if (options.serverUrl && options.apiKey) {
143
+ if (reqOptions.serverUrl && reqOptions.apiKey) {
132
144
  console.log("Publishing SBOM to Dependency Track");
133
- submitBom(options, bomNSData.bomJson);
145
+ submitBom(reqOptions, bomNSData.bomJson);
134
146
  }
135
147
  res.end("\n");
136
148
  if (cleanup && srcDir && srcDir.startsWith(os.tmpdir()) && fs.rmSync) {
package/utils.js CHANGED
@@ -4634,7 +4634,7 @@ export const parseCsPkgData = async function (pkgData) {
4634
4634
  attributesKey: "$",
4635
4635
  commentKey: "value"
4636
4636
  }).packages;
4637
- if (packages.length == 0) {
4637
+ if (!packages || packages.length == 0) {
4638
4638
  return pkgList;
4639
4639
  }
4640
4640
  packages = packages[0].package;
@@ -4661,7 +4661,7 @@ export const parseCsProjData = async function (csProjData) {
4661
4661
  attributesKey: "$",
4662
4662
  commentKey: "value"
4663
4663
  }).Project;
4664
- if (projects.length == 0) {
4664
+ if (!projects || projects.length == 0) {
4665
4665
  return pkgList;
4666
4666
  }
4667
4667
  const project = projects[0];
@@ -4719,6 +4719,9 @@ export const parseCsProjAssetsData = async function (csProjData) {
4719
4719
  const pkgList = [];
4720
4720
  let dependenciesList = [];
4721
4721
  let rootPkg = {};
4722
+ // This tracks the resolved version
4723
+ const pkgNameVersionMap = {};
4724
+ const pkgAddedMap = {};
4722
4725
 
4723
4726
  if (!csProjData) {
4724
4727
  return { pkgList, dependenciesList };
@@ -4784,12 +4787,12 @@ export const parseCsProjAssetsData = async function (csProjData) {
4784
4787
 
4785
4788
  if (csProjData.libraries && csProjData.targets) {
4786
4789
  const lib = csProjData.libraries;
4790
+ // Pass 1: Construct pkgList alone and track name and resolved version
4787
4791
  for (const framework in csProjData.targets) {
4788
4792
  for (const rootDep of Object.keys(csProjData.targets[framework])) {
4789
4793
  // if (rootDep.startsWith("runtime")){
4790
4794
  // continue;
4791
4795
  // }
4792
- const depList = new Set();
4793
4796
  const [name, version] = rootDep.split("/");
4794
4797
  const dpurl = decodeURIComponent(
4795
4798
  new PackageURL("nuget", "", name, version, null, null).toString()
@@ -4810,29 +4813,41 @@ export const parseCsProjAssetsData = async function (csProjData) {
4810
4813
  }
4811
4814
  }
4812
4815
  pkgList.push(pkg);
4813
-
4816
+ pkgNameVersionMap[name] = version;
4817
+ pkgAddedMap[name] = true;
4818
+ }
4819
+ }
4820
+ // Pass 2: Fix the dependency tree
4821
+ for (const framework in csProjData.targets) {
4822
+ for (const rootDep of Object.keys(csProjData.targets[framework])) {
4823
+ const depList = new Set();
4824
+ const [name, version] = rootDep.split("/");
4825
+ const dpurl = decodeURIComponent(
4826
+ new PackageURL("nuget", "", name, version, null, null).toString()
4827
+ );
4814
4828
  const dependencies =
4815
4829
  csProjData.targets[framework][rootDep].dependencies;
4816
4830
  if (dependencies) {
4817
4831
  for (const p of Object.keys(dependencies)) {
4832
+ // This condition is not required for assets json that are well-formed.
4833
+ if (!pkgNameVersionMap[p]) {
4834
+ continue;
4835
+ }
4836
+ let dversion = pkgNameVersionMap[p];
4818
4837
  const ipurl = decodeURIComponent(
4819
- new PackageURL(
4820
- "nuget",
4821
- "",
4822
- p,
4823
- dependencies[p],
4824
- null,
4825
- null
4826
- ).toString()
4838
+ new PackageURL("nuget", "", p, dversion, null, null).toString()
4827
4839
  );
4828
4840
  depList.add(ipurl);
4829
- pkgList.push({
4830
- group: "",
4831
- name: p,
4832
- version: dependencies[p],
4833
- description: "",
4834
- "bom-ref": ipurl
4835
- });
4841
+ if (!pkgAddedMap[p]) {
4842
+ pkgList.push({
4843
+ group: "",
4844
+ name: p,
4845
+ version: dversion,
4846
+ description: "",
4847
+ "bom-ref": ipurl
4848
+ });
4849
+ pkgAddedMap[p] = true;
4850
+ }
4836
4851
  }
4837
4852
  }
4838
4853
  dependenciesList.push({
package/utils.test.js CHANGED
@@ -1253,7 +1253,7 @@ test("parse project.assets.json", async () => {
1253
1253
  const dep_list = await parseCsProjAssetsData(
1254
1254
  readFileSync("./test/data/project.assets.json", { encoding: "utf-8" })
1255
1255
  );
1256
- expect(dep_list["pkgList"].length).toEqual(1460);
1256
+ expect(dep_list["pkgList"].length).toEqual(302);
1257
1257
  expect(dep_list["pkgList"][0]).toEqual({
1258
1258
  "bom-ref": "pkg:nuget/Castle.Core.Tests@0.0.0",
1259
1259
  group: "",