Skip to content

Instantly share code, notes, and snippets.

@imjasonh
Created July 2, 2025 01:29
Show Gist options
  • Save imjasonh/1a2e76f801bdebd99b568f80746a34ac to your computer and use it in GitHub Desktop.
Save imjasonh/1a2e76f801bdebd99b568f80746a34ac to your computer and use it in GitHub Desktop.
spdx2dot.go
package main
import (
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/spdx/tools-golang/json"
"github.com/spdx/tools-golang/spdx"
)
func main() {
var (
input = flag.String("input", "", "Path to SPDX JSON file")
output = flag.String("output", "", "Path to output DOT file (default: stdout)")
)
flag.Parse()
if *input == "" {
log.Fatal("Please provide an input SPDX file with -input")
}
// Read the SPDX file
data, err := os.ReadFile(*input)
if err != nil {
log.Fatalf("Failed to read input file: %v", err)
}
// Parse the SPDX document
doc, err := json.Read(strings.NewReader(string(data)))
if err != nil {
log.Fatalf("Failed to parse SPDX JSON: %v", err)
}
// Generate DOT output
dot := generateDot(doc)
// Write output
if *output != "" {
if err := os.WriteFile(*output, []byte(dot), 0644); err != nil {
log.Fatalf("Failed to write output file: %v", err)
}
fmt.Printf("Generated graphviz file: %s\n", *output)
} else {
fmt.Print(dot)
}
}
func generateDot(doc *spdx.Document) string {
var sb strings.Builder
// Start the digraph
sb.WriteString("digraph SPDX {\n")
sb.WriteString(" rankdir=TB;\n")
sb.WriteString(" node [shape=box, style=rounded];\n")
sb.WriteString(" edge [fontsize=10];\n\n")
// Add document node
sb.WriteString(fmt.Sprintf(" \"%s\" [label=\"Document\\n%s\", shape=folder, style=\"rounded,filled\", fillcolor=lightblue];\n\n",
doc.SPDXIdentifier, escape(doc.DocumentName)))
// Create a map of package IDs to names for easier lookup
packageNames := make(map[string]string)
if doc.Packages != nil {
for _, pkg := range doc.Packages {
packageNames[string(pkg.PackageSPDXIdentifier)] = pkg.PackageName
}
}
// Add package nodes
sb.WriteString(" // Packages\n")
if doc.Packages != nil {
for _, pkg := range doc.Packages {
label := fmt.Sprintf("Package: %s\\n%s", escape(pkg.PackageName), escape(pkg.PackageVersion))
if pkg.PackageSupplier != nil && pkg.PackageSupplier.Supplier != "" {
label += fmt.Sprintf("\\nSupplier: %s", escape(pkg.PackageSupplier.Supplier))
}
// Color OS packages differently
color := "lightgreen"
if strings.Contains(strings.ToLower(pkg.PackageName), "operating-system") {
color = "lightyellow"
}
sb.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\", style=\"rounded,filled\", fillcolor=%s];\n",
pkg.PackageSPDXIdentifier, label, color))
}
}
sb.WriteString("\n")
// Add relationships
sb.WriteString(" // Relationships\n")
if doc.Relationships != nil {
for _, rel := range doc.Relationships {
// Skip certain relationship types for clarity
if rel.Relationship == "DESCRIBES" && rel.RefA.ElementRefID == doc.SPDXIdentifier {
continue
}
// Create edge with relationship type as label
edgeStyle := ""
switch rel.Relationship {
case "CONTAINS":
edgeStyle = ", color=blue, style=bold"
case "GENERATED_FROM":
edgeStyle = ", color=red"
case "DESCRIBED_BY":
edgeStyle = ", color=green, style=dashed"
}
sb.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\" [label=\"%s\"%s];\n",
rel.RefA.ElementRefID, rel.RefB.ElementRefID, rel.Relationship, edgeStyle))
}
}
// Add legend
sb.WriteString("\n // Legend\n")
sb.WriteString(" subgraph cluster_legend {\n")
sb.WriteString(" label=\"Legend\";\n")
sb.WriteString(" style=dotted;\n")
sb.WriteString(" node [shape=plaintext];\n")
sb.WriteString(" legend [label=<\n")
sb.WriteString(" <TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n")
sb.WriteString(" <TR><TD BGCOLOR=\"lightblue\">Document</TD></TR>\n")
sb.WriteString(" <TR><TD BGCOLOR=\"lightgreen\">Package</TD></TR>\n")
sb.WriteString(" <TR><TD BGCOLOR=\"lightyellow\">OS Package</TD></TR>\n")
sb.WriteString(" <TR><TD>Blue Bold → CONTAINS</TD></TR>\n")
sb.WriteString(" <TR><TD>Red → GENERATED_FROM</TD></TR>\n")
sb.WriteString(" <TR><TD>Green Dashed → DESCRIBED_BY</TD></TR>\n")
sb.WriteString(" </TABLE>\n")
sb.WriteString(" >];\n")
sb.WriteString(" }\n")
sb.WriteString("}\n")
return sb.String()
}
func escape(s string) string {
// Escape special characters for DOT labels
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "\"", "\\\"")
s = strings.ReplaceAll(s, "\n", "\\n")
return s
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment