Skip to content

Commit 961baa1

Browse files
sbom drift and format and diff
1 parent 07ed8a9 commit 961baa1

11 files changed

Lines changed: 4498 additions & 4 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ scripts/swap/swap
2727
cmd/aliases/.go-version
2828

2929
sbom*.json
30+
31+
.goenv/*

cmd/compliance/sbom_diff.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package compliance
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/go-nv/goenv/internal/sbom"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var (
12+
diffFormat string
13+
diffShowUnchanged bool
14+
diffShowOnly string
15+
diffFailOn string
16+
diffColor bool
17+
diffOutput string
18+
)
19+
20+
var sbomDiffCmd = &cobra.Command{
21+
Use: "diff <old-sbom> <new-sbom>",
22+
Short: "Compare two SBOMs and show differences",
23+
Long: `Compare two SBOM files and display the differences.
24+
25+
This command analyzes changes between two SBOMs, including:
26+
- Added dependencies
27+
- Removed dependencies
28+
- Version changes (upgrades/downgrades)
29+
- License changes
30+
31+
Useful for:
32+
- Release change tracking
33+
- Security impact analysis
34+
- Dependency drift detection
35+
- CI/CD validation
36+
37+
Phase 5: Automation & Compliance (v3.5)
38+
39+
Examples:
40+
# Basic comparison
41+
goenv sbom diff sbom-v1.0.0.json sbom-v1.1.0.json
42+
43+
# JSON output for automation
44+
goenv sbom diff old.json new.json --format=json
45+
46+
# GitHub Actions format
47+
goenv sbom diff old.json new.json --format=github
48+
49+
# Show only additions
50+
goenv sbom diff old.json new.json --show=added
51+
52+
# Fail if dependencies were removed
53+
goenv sbom diff old.json new.json --fail-on=removed
54+
55+
# Save to file
56+
goenv sbom diff old.json new.json -o diff-report.md --format=markdown`,
57+
Args: cobra.ExactArgs(2),
58+
RunE: runSBOMDiff,
59+
}
60+
61+
func init() {
62+
sbomDiffCmd.Flags().StringVarP(&diffFormat, "format", "f", "table",
63+
"Output format: table, json, github, markdown")
64+
sbomDiffCmd.Flags().BoolVarP(&diffShowUnchanged, "show-unchanged", "u", false,
65+
"Show unchanged components (table format only)")
66+
sbomDiffCmd.Flags().StringVar(&diffShowOnly, "show", "all",
67+
"Show only specific changes: all, added, removed, modified")
68+
sbomDiffCmd.Flags().StringVar(&diffFailOn, "fail-on", "",
69+
"Exit with error if condition met: added, removed, modified, downgrade, license-change")
70+
sbomDiffCmd.Flags().BoolVar(&diffColor, "color", true,
71+
"Use colored output (table format only, auto-detected for TTY)")
72+
sbomDiffCmd.Flags().StringVarP(&diffOutput, "output", "o", "",
73+
"Write output to file instead of stdout")
74+
75+
sbomCmd.AddCommand(sbomDiffCmd)
76+
}
77+
78+
func runSBOMDiff(cmd *cobra.Command, args []string) error {
79+
oldPath := args[0]
80+
newPath := args[1]
81+
82+
// Validate input files exist
83+
if _, err := os.Stat(oldPath); err != nil {
84+
return fmt.Errorf("old SBOM file not found: %s", oldPath)
85+
}
86+
if _, err := os.Stat(newPath); err != nil {
87+
return fmt.Errorf("new SBOM file not found: %s", newPath)
88+
}
89+
90+
// Prepare diff options
91+
opts := &sbom.DiffOptions{
92+
ShowUnchanged: diffShowUnchanged,
93+
}
94+
95+
// Perform diff
96+
result, err := sbom.DiffSBOMs(oldPath, newPath, opts)
97+
if err != nil {
98+
return fmt.Errorf("failed to diff SBOMs: %w", err)
99+
}
100+
101+
// Filter results if requested
102+
if diffShowOnly != "all" {
103+
result = filterDiffResult(result, diffShowOnly)
104+
}
105+
106+
// Auto-detect color support if not explicitly set
107+
useColor := diffColor
108+
if !cmd.Flags().Changed("color") {
109+
useColor = isTerminal()
110+
}
111+
112+
// Get formatter
113+
formatter, err := sbom.GetFormatter(diffFormat, diffShowUnchanged, useColor)
114+
if err != nil {
115+
return err
116+
}
117+
118+
// Determine output destination
119+
var output *os.File
120+
if diffOutput != "" {
121+
f, err := os.Create(diffOutput)
122+
if err != nil {
123+
return fmt.Errorf("failed to create output file: %w", err)
124+
}
125+
defer f.Close()
126+
output = f
127+
} else {
128+
output = os.Stdout
129+
}
130+
131+
// Format and write output
132+
if err := formatter.Format(result, output); err != nil {
133+
return fmt.Errorf("failed to format diff: %w", err)
134+
}
135+
136+
// Check fail conditions
137+
if diffFailOn != "" {
138+
if shouldFail(result, diffFailOn) {
139+
if diffOutput == "" {
140+
fmt.Fprintf(os.Stderr, "\n")
141+
}
142+
return fmt.Errorf("diff check failed: condition '%s' met", diffFailOn)
143+
}
144+
}
145+
146+
return nil
147+
}
148+
149+
func filterDiffResult(result *sbom.DiffResult, showOnly string) *sbom.DiffResult {
150+
filtered := &sbom.DiffResult{
151+
Summary: result.Summary,
152+
Comparison: result.Comparison,
153+
}
154+
155+
switch showOnly {
156+
case "added":
157+
filtered.Added = result.Added
158+
case "removed":
159+
filtered.Removed = result.Removed
160+
case "modified":
161+
filtered.Modified = result.Modified
162+
default:
163+
// Return original
164+
return result
165+
}
166+
167+
return filtered
168+
}
169+
170+
func shouldFail(result *sbom.DiffResult, condition string) bool {
171+
switch condition {
172+
case "added":
173+
return result.Summary.AddedCount > 0
174+
case "removed":
175+
return result.Summary.RemovedCount > 0
176+
case "modified":
177+
return result.Summary.ModifiedCount > 0
178+
case "downgrade":
179+
return result.Summary.VersionDowngrades > 0
180+
case "license-change":
181+
return result.Summary.LicenseChanges > 0
182+
case "any":
183+
return result.Summary.AddedCount > 0 ||
184+
result.Summary.RemovedCount > 0 ||
185+
result.Summary.ModifiedCount > 0
186+
default:
187+
return false
188+
}
189+
}
190+
191+
func isTerminal() bool {
192+
// Check if stdout is a terminal
193+
fileInfo, err := os.Stdout.Stat()
194+
if err != nil {
195+
return false
196+
}
197+
return (fileInfo.Mode() & os.ModeCharDevice) != 0
198+
}

0 commit comments

Comments
 (0)