Repository: CycloneDX/cdxgen (js)

Issue:
Dependency tree for dotnet
https://github.com/CycloneDX/cdxgen/blob/master/test/data/project.assets.json

Has dependencies section

Pull request:
Feature/501 dependency tree for dotnet
Closes #501 

Adds parsing of dependency info from package.assets.json targets, including target dependency dependencies.

Patch:
diff --git a/index.js b/index.js
index 3d6b719e..6352cab9 100644
--- a/index.js
+++ b/index.js
@@ -3913,6 +3913,32 @@ export const createRubyBom = async (path, options) => {
   return {};
 };
 
+const removeDuplicates = (pkgList, dependencies) => {
+  const uniqueItems = {};
+  const uniqueRefs = new Set();
+  const newPkgList = []
+  const newDependencies = [];
+
+  for (const item of pkgList) {
+    if (item) {
+      const {name, version} = item;
+      const key = `${name}-${version}`;
+      if (!uniqueItems[key] && key) {
+        uniqueItems[key] = item;
+        newPkgList.push(item)
+      }
+    }
+  }
+
+  for (const item of dependencies) {
+    const {ref} = item;
+    if (!uniqueRefs.has(ref)) {
+      uniqueRefs.add(ref);
+      newDependencies.push(item);
+    }
+  }
+  return [newPkgList, newDependencies];
+};
 /**
  * Function to create bom string for csharp projects
  *
@@ -3922,6 +3948,7 @@ export const createRubyBom = async (path, options) => {
 export const createCsharpBom = async (path, options) => {
   let manifestFiles = [];
   let pkgData = undefined;
+  let dependencies = []
   const csProjFiles = getAllFiles(
     path,
     (options.multiProject ? "**/" : "") + "*.csproj"
@@ -3963,10 +3990,15 @@ export const createCsharpBom = async (path, options) => {
         console.log(`Parsing ${af}`);
       }
       pkgData = readFileSync(af, { encoding: "utf-8" });
-      const dlist = await parseCsProjAssetsData(pkgData);
+      let results = await parseCsProjAssetsData(pkgData);
+      let deps = results["dependenciesList"];
+      let dlist = results["pkgList"];
       if (dlist && dlist.length) {
         pkgList = pkgList.concat(dlist);
       }
+      if (deps && deps.length) {
+        dependencies = dependencies.concat(deps);
+      }
     }
   } else if (pkgLockFiles.length) {
     manifestFiles = manifestFiles.concat(pkgLockFiles);
@@ -4017,9 +4049,13 @@ export const createCsharpBom = async (path, options) => {
     }
   }
   if (pkgList.length) {
+    const uniquePkg = removeDuplicates(pkgList,dependencies);
+    dependencies = uniquePkg[1];
+    pkgList = uniquePkg[0];
     return buildBomNSData(options, pkgList, "nuget", {
       src: path,
-      filename: manifestFiles.join(", ")
+      filename: manifestFiles.join(", "),
+      dependencies
     });
   }
   return {};
diff --git a/utils.js b/utils.js
index 7307798d..c51868b8 100644
--- a/utils.js
+++ b/utils.js
@@ -4373,42 +4373,122 @@ export const parseCsProjData = async function (csProjData) {
 
 export const parseCsProjAssetsData = async function (csProjData) {
   const pkgList = [];
-  let pkg = null;
-  if (!csProjData) {
-    return pkgList;
-  }
-  const assetData = JSON.parse(csProjData);
-  if (!assetData || !assetData.libraries) {
-    return pkgList;
+  let dependenciesList = [];
+  let rootPkg = {};
+
+  if (!csProjData){
+    return pkgList, dependenciesList
   }
-  for (const alib of Object.keys(assetData.libraries)) {
-    // Skip os runtime packages
-    if (alib.startsWith("runtime")) {
-      continue;
+
+  csProjData = JSON.parse(csProjData);
+  rootPkg =
+    {
+      group: "",
+      name: csProjData.project.restore.projectName,
+      version: csProjData.project.version || "latest",
+      type: "application",
+      "bom-ref": decodeURIComponent(
+        new PackageURL(
+          "nuget",
+          "",
+          csProjData.project.restore.projectName,
+          csProjData.project.version || "latest",
+          null,
+          null
+        ).toString()
+      )
     }
-    const tmpA = alib.split("/");
-    const libData = assetData.libraries[alib];
-    if (tmpA.length > 1) {
-      pkg = {
-        group: "",
-        name: tmpA[0],
-        version: tmpA[tmpA.length - 1]
-      };
-      if (libData.sha256) {
-        pkg._integrity = "sha256-" + libData.sha256;
-      }
-      if (libData.sha512) {
-        pkg._integrity = "sha512-" + libData.sha512;
+
+  const purlString = decodeURIComponent(rootPkg["bom-ref"].toString());
+  pkgList.push(rootPkg);
+  let rootPkgDeps = new Set();
+
+  if (
+    csProjData.libraries &&
+    csProjData.targets
+  ) {
+    const lib = csProjData.libraries;
+    for (const framework in csProjData.targets) {
+      for (const rootDep of Object.keys(csProjData.targets[framework])) {
+        // if (rootDep.startsWith("runtime")){
+        //   continue;
+        // }
+        const depList = new Set();
+        const [name, version] = rootDep.split("/");
+        const dpurl = decodeURIComponent(
+          new PackageURL(
+            "nuget",
+            "",
+            name,
+            version,
+            null,
+            null
+          ).toString()
+        );
+        let pkg = {
+            group: "",
+            name: name,
+            version: version,
+            description: "",
+            type: csProjData.targets[framework][rootDep].type,
+            "bom-ref": dpurl,
+          }
+        if (lib[rootDep]) {
+          if (lib[rootDep].sha512){
+            pkg["_integrity"] = "sha512-" + lib[rootDep].sha512;
+          }
+          else if (lib[rootDep].sha256){
+            pkg["_integrity"] = "sha256-" + lib[rootDep].sha256;
+          }
+        }
+        pkgList.push(pkg);
+        rootPkgDeps.add(dpurl);
+
+        const dependencies = csProjData.targets[framework][rootDep].dependencies;
+        if (dependencies) {
+          for (const p of Object.keys(dependencies)) {
+            const ipurl = decodeURIComponent(
+              new PackageURL(
+                "nuget",
+                "",
+                p,
+                dependencies[p],
+                null,
+                null
+              ).toString()
+            );
+            depList.add(ipurl);
+            pkgList.push(
+              {
+                group: "",
+                name: p,
+                version: dependencies[p],
+                description: "",
+                "bom-ref": ipurl
+              }
+            )
+          }
+        }
+        dependenciesList.push({
+          ref: dpurl,
+          dependsOn: Array.from(depList)
+        });
       }
-      pkgList.push(pkg);
+      dependenciesList.push({
+        ref: purlString,
+        dependsOn: Array.from(rootPkgDeps)
+      })
     }
   }
   if (fetchLicenses) {
     return await getNugetMetadata(pkgList);
   } else {
-    return pkgList;
+    return {
+      pkgList,
+      dependenciesList
+    }
   }
-};
+}
 
 export const parseCsPkgLockData = async function (csLockData) {
   const pkgList = [];


Test patch:
diff --git a/utils.test.js b/utils.test.js
index 3afdd3ad..df6d26c2 100644
--- a/utils.test.js
+++ b/utils.test.js
@@ -1180,14 +1180,30 @@ test("parse project.assets.json", async () => {
   const dep_list = await parseCsProjAssetsData(
     readFileSync("./test/data/project.assets.json", { encoding: "utf-8" })
   );
-  expect(dep_list.length).toEqual(142);
-  expect(dep_list[0]).toEqual({
-    group: "",
-    name: "Castle.Core",
-    version: "4.4.1",
-    _integrity:
-      "sha512-zanbjWC0Y05gbx4eGXkzVycOQqVOFVeCjVsDSyuao9P4mtN1w3WxxTo193NGC7j3o2u3AJRswaoC6hEbnGACnQ=="
-  });
+  expect(dep_list["pkgList"].length).toEqual(812);
+  expect(dep_list["pkgList"][0]).toEqual({
+    "bom-ref": "pkg:nuget/GoatlyCore@1.0.0",
+    "group": "",
+    "name": "GoatlyCore",
+    "type": "application",
+    "version": "1.0.0"
+  });
+  expect(dep_list["dependenciesList"].length).toEqual(163);
+  expect(dep_list["dependenciesList"][0]).toEqual({
+    "dependsOn": [
+    "pkg:nuget/NETStandard.Library@1.6.1",
+    "pkg:nuget/System.Collections.Specialized@4.3.0",
+    "pkg:nuget/System.ComponentModel@4.3.0",
+    "pkg:nuget/System.ComponentModel.TypeConverter@4.3.0",
+    "pkg:nuget/System.Diagnostics.TraceSource@4.3.0",
+    "pkg:nuget/System.Dynamic.Runtime@4.3.0",
+    "pkg:nuget/System.Reflection@4.3.0",
+    "pkg:nuget/System.Reflection.Emit@4.3.0",
+    "pkg:nuget/System.Reflection.TypeExtensions@4.3.0",
+    "pkg:nuget/System.Xml.XmlDocument@4.3.0"
+  ],
+    "ref": "pkg:nuget/Castle.Core@4.4.1"
+  })
 });
 
 test("parse packages.lock.json", async () => {