Created
July 2, 2025 01:29
-
-
Save imjasonh/1a2e76f801bdebd99b568f80746a34ac to your computer and use it in GitHub Desktop.
spdx2dot.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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