Skip to content

Commit b2d89cf

Browse files
authored
Merge pull request #6 from EdgarPsda/v0.5.0/sbom-generation
Add SBOM generation command
2 parents 4cf0c46 + 76fdf42 commit b2d89cf

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

cli/cmd/sbom.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"runtime"
9+
"strings"
10+
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var (
15+
sbomFormat string
16+
sbomOutput string
17+
sbomOpen bool
18+
)
19+
20+
var sbomCmd = &cobra.Command{
21+
Use: "sbom",
22+
Short: "Generate a Software Bill of Materials (SBOM)",
23+
Long: `Generate an SBOM for your project using Trivy.
24+
25+
Supports CycloneDX (default) and SPDX formats. The SBOM lists all
26+
dependencies, licenses, and component metadata for compliance and
27+
supply chain security.`,
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
return runSBOM()
30+
},
31+
}
32+
33+
func init() {
34+
rootCmd.AddCommand(sbomCmd)
35+
36+
sbomCmd.Flags().StringVar(&sbomFormat, "format", "cyclonedx", "SBOM format: cyclonedx, spdx")
37+
sbomCmd.Flags().StringVar(&sbomOutput, "output", "", "Output file path (default: sbom-<format>.json)")
38+
sbomCmd.Flags().BoolVar(&sbomOpen, "open", false, "Auto-open the SBOM file after generation")
39+
}
40+
41+
func runSBOM() error {
42+
// Check if Trivy is installed
43+
if _, err := exec.LookPath("trivy"); err != nil {
44+
return fmt.Errorf("trivy is not installed. Install with: brew install trivy or visit https://github.com/aquasecurity/trivy")
45+
}
46+
47+
// Get working directory
48+
dir, err := os.Getwd()
49+
if err != nil {
50+
return fmt.Errorf("failed to get working directory: %w", err)
51+
}
52+
53+
// Resolve SBOM format
54+
trivyFormat, err := resolveTrivyFormat(sbomFormat)
55+
if err != nil {
56+
return err
57+
}
58+
59+
// Resolve output file path
60+
outputPath := sbomOutput
61+
if outputPath == "" {
62+
outputPath = fmt.Sprintf("sbom-%s.json", sbomFormat)
63+
}
64+
65+
fmt.Printf("📦 Generating SBOM (%s format)...\n", sbomFormat)
66+
67+
// Build Trivy SBOM command
68+
cmd := exec.Command("trivy", "fs", ".", "--format", trivyFormat, "--output", outputPath)
69+
cmd.Dir = dir
70+
cmd.Stdout = os.Stdout
71+
cmd.Stderr = os.Stderr
72+
73+
if err := cmd.Run(); err != nil {
74+
return fmt.Errorf("SBOM generation failed: %w", err)
75+
}
76+
77+
// Verify file was created
78+
absPath, err := filepath.Abs(outputPath)
79+
if err != nil {
80+
absPath = outputPath
81+
}
82+
83+
info, err := os.Stat(absPath)
84+
if err != nil {
85+
return fmt.Errorf("SBOM file was not created: %w", err)
86+
}
87+
88+
fmt.Printf("✅ SBOM generated: %s (%s)\n", outputPath, formatFileSize(info.Size()))
89+
fmt.Printf(" Format: %s\n", strings.ToUpper(sbomFormat))
90+
91+
if sbomOpen {
92+
openSBOMFile(absPath)
93+
fmt.Println("🌐 Opening SBOM file...")
94+
}
95+
96+
return nil
97+
}
98+
99+
// resolveTrivyFormat maps user-facing format names to Trivy format flags
100+
func resolveTrivyFormat(format string) (string, error) {
101+
switch strings.ToLower(format) {
102+
case "cyclonedx":
103+
return "cyclonedx", nil
104+
case "spdx":
105+
return "spdx-json", nil
106+
default:
107+
return "", fmt.Errorf("unsupported SBOM format: %s (supported: cyclonedx, spdx)", format)
108+
}
109+
}
110+
111+
// formatFileSize returns a human-readable file size
112+
func formatFileSize(bytes int64) string {
113+
const unit = 1024
114+
if bytes < unit {
115+
return fmt.Sprintf("%d B", bytes)
116+
}
117+
div, exp := int64(unit), 0
118+
for n := bytes / unit; n >= unit; n /= unit {
119+
div *= unit
120+
exp++
121+
}
122+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
123+
}
124+
125+
// openSBOMFile opens the SBOM file in the default application
126+
func openSBOMFile(path string) {
127+
var cmd *exec.Cmd
128+
129+
switch runtime.GOOS {
130+
case "darwin":
131+
cmd = exec.Command("open", path)
132+
case "linux":
133+
cmd = exec.Command("xdg-open", path)
134+
case "windows":
135+
cmd = exec.Command("cmd", "/c", "start", path)
136+
}
137+
138+
if cmd != nil {
139+
_ = cmd.Start()
140+
}
141+
}

0 commit comments

Comments
 (0)