cyclonedx-cli icon indicating copy to clipboard operation
cyclonedx-cli copied to clipboard

Add support for dependency graph visualization

Open nscuro opened this issue 4 years ago • 11 comments

When working with dependencies, it's important to understand how they're introduced. Since CycloneDX 1.2, dependency graphs are part of the core spec. For previous spec versions, there is a dependency graph extension.

The graph as included in CycloneDX BOMs is, while simple and minimalistic, hard to parse for humans. cyclonedx-cli should include a command that visualizes the graph in some sort of tree structure.

For example, executing for mvn dependency:tree for Alpine produces the following output:

us.springett:alpine:jar:1.8.0-SNAPSHOT
+- commons-io:commons-io:jar:2.6:compile
+- org.apache.commons:commons-lang3:jar:3.10:compile
+- org.apache.commons:commons-collections4:jar:4.4:compile
+- org.glassfish.jersey.core:jersey-client:jar:2.29.1:compile
|  +- jakarta.ws.rs:jakarta.ws.rs-api:jar:2.1.6:compile
|  +- org.glassfish.jersey.core:jersey-common:jar:2.29.1:compile
|  |  +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
|  |  +- org.glassfish.hk2:osgi-resource-locator:jar:1.0.3:compile
|  |  \- com.sun.activation:jakarta.activation:jar:1.2.1:compile
|  \- org.glassfish.hk2.external:jakarta.inject:jar:2.6.1:compile
+- javax.servlet:javax.servlet-api:jar:4.0.1:provided
+- org.glassfish.jersey.containers:jersey-container-servlet:jar:2.29.1:compile
|  +- org.glassfish.jersey.containers:jersey-container-servlet-core:jar:2.29.1:compile
|  \- org.glassfish.jersey.core:jersey-server:jar:2.29.1:compile
|     +- org.glassfish.jersey.media:jersey-media-jaxb:jar:2.29.1:compile
|     \- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.2:compile
|        \- jakarta.activation:jakarta.activation-api:jar:1.2.1:compile
...

The main focus should be on terminal output. For the future, it may also be helpful to transpile CDX's dependency graph into the DOT language, which would allow generation of graph images with GraphViz.

nscuro avatar Nov 01 '20 14:11 nscuro

Thanks for the well detailed issue!

My current plan for sub-commands like this, that produce terminal output, is to have an optional output-format parameter. This will default to text which is basic terminal output. But it will also support other machine readable formats.

I think JSON probably makes sense as the first format to support. It's easy to implement and aligns with one of the existing CycloneDX SBOM formats.

@nscuro @stevespringett do you have any opinions on what the first machine readable format should be? This is specifically to support automation scenarios.

coderpatros avatar Nov 01 '20 20:11 coderpatros

Good to see that you have automatability in mind from the very beginning. I think JSON is a great choice.

nscuro avatar Nov 01 '20 20:11 nscuro

Would output be able to highlight when there are more than one version of the same component (eg, commons-codec).

In my maven projects I have seen that the shaded jar only includes one version and it's almost always the most recent one... but not always. Thus, there is a need to identify dependency version clashes so that they can be resolved.

Also, would it be possible to identify which components are optional and which required?

If these seem like worthwhile things to do, but not for an MVP, then I can log new issues to cover them.

msymons avatar Nov 04 '20 15:11 msymons

@msymons The CLI doesn't know what Maven is. Every dependency management tool has a slightly different algorithm for resolving component versions. Maven uses a nearest neighbor method. The tree support in the CLI will only visualize the dependency graph from a provided BOM, so it's not going to know any of the low-level dependency management details. You'll have to use ecosystem specific tools for that.

However, I think identifying component scope (required, optional, excluded) would be a good enhancement.

stevespringett avatar Nov 04 '20 16:11 stevespringett

I understand that the CLI does not know what Maven is. Or npm, etc It only cares about the BOM. However, a BOM can still end up containing multiple versions of the same component. eg:

pkg:maven/org.apache.httpcomponents/[email protected]?type=jar
pkg:maven/org.apache.httpcomponents/[email protected]?type=jar

Dependency resolution in the build has meant that only one of these versions is actually valid (used). But both end up in the BOM. As it happens, one version has a vulnerability (CVE-2020-13956) and one (the newer) does not. Furthermore, tools such OSS Index are not yet reporting the vulnerability. This means that vulnerability alerting in (say) Dependency-Track would not help highlight that an older version of a component is still trying to sneak in.

Hence, all I am seeking is a means of quickly spotting (visualizing) when such dependency version clashes have occurred. Because spotting them is half the battle. Fixing such a clash is pretty easy once they are spotted.

I have BOMs with over 3000 lines (300 components) and manually checking for clashes is a pain.

I see this functionality as being useful for when someone is double checking output from an earlier stage in a DevSecOps pipeline and the someone (me, in this case) is not a developer or might not have access to build logs, etc.

msymons avatar Nov 05 '20 13:11 msymons

Hi @msymons I have been thinking about some way to query, and modify, the SBOM using this tool. It could help with issue #14 to be able to modify components.

coderpatros avatar Nov 05 '20 21:11 coderpatros

Checking on this enhancement request. At the moment, it doesn't appears as though cyclonedx-cli's 'add' command will generate dependency graph information in a resulting sbom file. (Perhaps I'm overlooking something?)

Is the inclusion of dependency graph information still a forthcoming enhancement?

twright-0x1 avatar Jan 25 '23 18:01 twright-0x1

I wrote a CycloneDX SBOM viewer using HTMl, JavaScript, Bootstrap and DataTables. The viewer also tries to visualize the dependencies section in a HTML unorderered list, for similar reasons already outlined above. Here's how that looks like:

image

As my code sometimes fails to create the dependency tree recursively (RangeError: Maximum call stack size exceeded), I searched for existing implementations of visualizing the dependencies section of a CycloneDX SBOM, and found this issue.

Is there a visualization feature meanwhile in the CLI or any other related tool?

sgustafsson avatar Mar 06 '24 14:03 sgustafsson

@sgustafsson You're likely running into this due to circular dependency relationships. When recursing, maintain a list or stack of BOM refs you encountered. For each recursion step, check if you already saw the BOM ref at hand. Terminate the recursion if you did.

This has not yet been added to the CLI, but https://dependencytrack.org/ has a visualization that can handle circular dependencies. It operates on its own data model though, not strictly CycloneDX BOMs.

nscuro avatar Mar 06 '24 15:03 nscuro

That is my suspicion too. It happens only in case of "big" dependencies section of SBOMS for npm packages, like https://github.com/CycloneDX/cyclonedx-node-npm/blob/main/demo/juice-shop/example-results/flat/bom.1.5.json. I'll try to tweak my code, or tell GH CoPilot to do it for me :smirk:

sgustafsson avatar Mar 06 '24 15:03 sgustafsson

Just in case someone finds this issue and also wants to visualize dependencies, here's what I use now:

  • For visualizing a tree based on the dependencies: https://github.com/square/dependentree
  • For listing all cycles: https://www.npmjs.com/package/graph-cycles

graph-cycles example code here:

import fs from 'fs';
import { analyzeGraph } from 'graph-cycles'

let cyclonedxbom = JSON.parse(fs.readFileSync('bom.json', 'utf8'));

let graph = [];

for (let i = 0; i < cyclonedxbom.dependencies.length; i++) {
    let dependency = cyclonedxbom.dependencies[i];
    let newGraphEntry = [];
    newGraphEntry.push(dependency.ref);
    let depsArray = [];
    if(!dependency.dependsOn) {
        depsArray.push("]");
    } else {
        for (let j = 0; j < dependency.dependsOn.length; j++) {
            let subDependency = dependency.dependsOn[j];
            depsArray.push(subDependency);
        }
    }    
    newGraphEntry.push(depsArray);
    graph.push(newGraphEntry);
}

const analysis = analyzeGraph( graph );
console.log(analysis.cycles);

sgustafsson avatar Mar 08 '24 11:03 sgustafsson