Skip to content

Commit fbaded7

Browse files
Add first cut of SBOM impl
1 parent 2abf854 commit fbaded7

3 files changed

Lines changed: 835 additions & 8 deletions

File tree

cmd/compliance/sbom.go

Lines changed: 159 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"github.com/go-nv/goenv/internal/cmdutil"
1212
"github.com/go-nv/goenv/internal/config"
1313
"github.com/go-nv/goenv/internal/errors"
14+
"github.com/go-nv/goenv/internal/manager"
1415
"github.com/go-nv/goenv/internal/platform"
16+
"github.com/go-nv/goenv/internal/sbom"
1517
"github.com/go-nv/goenv/internal/utils"
1618
"github.com/spf13/cobra"
1719
)
@@ -72,14 +74,17 @@ Supported tools:
7274
}
7375

7476
var (
75-
sbomTool string
76-
sbomFormat string
77-
sbomOutput string
78-
sbomDir string
79-
sbomImage string
80-
sbomModulesOnly bool
81-
sbomOffline bool
82-
sbomToolArgs string
77+
sbomTool string
78+
sbomFormat string
79+
sbomOutput string
80+
sbomDir string
81+
sbomImage string
82+
sbomModulesOnly bool
83+
sbomOffline bool
84+
sbomToolArgs string
85+
sbomDeterministic bool
86+
sbomEmbedDigests bool
87+
sbomEnhance bool
8388
)
8489

8590
func init() {
@@ -91,11 +96,132 @@ func init() {
9196
sbomProjectCmd.Flags().BoolVar(&sbomModulesOnly, "modules-only", false, "Only scan Go modules (cyclonedx-gomod)")
9297
sbomProjectCmd.Flags().BoolVar(&sbomOffline, "offline", false, "Offline mode - avoid network access")
9398
sbomProjectCmd.Flags().StringVar(&sbomToolArgs, "tool-args", "", "Additional arguments to pass to the tool")
99+
sbomProjectCmd.Flags().BoolVar(&sbomEnhance, "enhance", true, "Add Go-aware metadata to SBOM (default true)")
100+
sbomProjectCmd.Flags().BoolVar(&sbomDeterministic, "deterministic", false, "Generate deterministic/reproducible SBOM")
101+
sbomProjectCmd.Flags().BoolVar(&sbomEmbedDigests, "embed-digests", false, "Embed go.mod/go.sum digests for reproducibility")
94102

95103
sbomCmd.AddCommand(sbomProjectCmd)
104+
sbomCmd.AddCommand(sbomHashCmd)
105+
sbomCmd.AddCommand(sbomVerifyCmd)
96106
cmdpkg.RootCmd.AddCommand(sbomCmd)
97107
}
98108

109+
var sbomHashCmd = &cobra.Command{
110+
Use: "hash <sbom-file>",
111+
Short: "Compute digest of an SBOM file",
112+
Long: `Compute a cryptographic hash of an SBOM file for reproducibility verification.
113+
114+
This command normalizes the SBOM (sorting components, normalizing whitespace) before
115+
computing the digest to ensure consistent hashing across different generation runs.
116+
117+
The digest can be used to verify that two SBOMs have identical semantic content,
118+
even if they were generated at different times or with different metadata timestamps.
119+
120+
Examples:
121+
# Compute hash of an SBOM
122+
goenv sbom hash sbom.json
123+
124+
# Compute hash with specific algorithm
125+
goenv sbom hash sbom.json --algorithm=sha512`,
126+
Args: cobra.ExactArgs(1),
127+
RunE: runSBOMHash,
128+
}
129+
130+
var sbomVerifyCmd = &cobra.Command{
131+
Use: "verify-reproducible <sbom1> <sbom2>",
132+
Short: "Verify two SBOMs have identical reproducible content",
133+
Long: `Compare two SBOM files to verify they have identical semantic content.
134+
135+
This command normalizes both SBOMs (removing timestamps, sorting components) and
136+
compares their content digests to verify reproducibility. Exit code 0 indicates
137+
the SBOMs are identical, non-zero indicates differences.
138+
139+
This is useful for:
140+
- Verifying deterministic SBOM generation in CI/CD
141+
- Detecting unexpected changes in dependencies
142+
- Validating reproducible builds
143+
144+
Examples:
145+
# Compare two SBOMs
146+
goenv sbom verify-reproducible sbom1.json sbom2.json
147+
148+
# Verify with detailed diff output
149+
goenv sbom verify-reproducible sbom1.json sbom2.json --diff`,
150+
Args: cobra.ExactArgs(2),
151+
RunE: runSBOMVerify,
152+
}
153+
154+
var (
155+
hashAlgorithm string
156+
verifyDiff bool
157+
)
158+
159+
func init() {
160+
sbomHashCmd.Flags().StringVar(&hashAlgorithm, "algorithm", "sha256", "Hash algorithm (sha256, sha512)")
161+
sbomVerifyCmd.Flags().BoolVar(&verifyDiff, "diff", false, "Show detailed differences if SBOMs don't match")
162+
}
163+
164+
func runSBOMHash(cmd *cobra.Command, args []string) error {
165+
sbomPath := args[0]
166+
167+
// Verify file exists
168+
if !utils.FileExists(sbomPath) {
169+
return fmt.Errorf("SBOM file not found: %s", sbomPath)
170+
}
171+
172+
// Compute hash using the enhancer's deterministic logic
173+
cfg, _ := cmdutil.SetupContext()
174+
hash, err := sbom.ComputeSBOMDigest(sbomPath, hashAlgorithm)
175+
if err != nil {
176+
return errors.FailedTo("compute SBOM digest", err)
177+
}
178+
179+
// Output hash in format: <algorithm>:<hex-digest>
180+
if cfg.Debug {
181+
fmt.Fprintf(cmd.OutOrStdout(), "%s:%s %s\n", hashAlgorithm, hash, sbomPath)
182+
} else {
183+
fmt.Fprintf(cmd.OutOrStdout(), "%s:%s\n", hashAlgorithm, hash)
184+
}
185+
186+
return nil
187+
}
188+
189+
func runSBOMVerify(cmd *cobra.Command, args []string) error {
190+
sbom1Path := args[0]
191+
sbom2Path := args[1]
192+
193+
// Verify both files exist
194+
if !utils.FileExists(sbom1Path) {
195+
return fmt.Errorf("SBOM file not found: %s", sbom1Path)
196+
}
197+
if !utils.FileExists(sbom2Path) {
198+
return fmt.Errorf("SBOM file not found: %s", sbom2Path)
199+
}
200+
201+
// Compare SBOMs
202+
cfg, _ := cmdutil.SetupContext()
203+
match, diff, err := sbom.VerifyReproducible(sbom1Path, sbom2Path)
204+
if err != nil {
205+
return errors.FailedTo("verify reproducibility", err)
206+
}
207+
208+
if match {
209+
fmt.Fprintf(cmd.OutOrStdout(), "✓ SBOMs are reproducibly identical\n")
210+
if cfg.Debug {
211+
fmt.Fprintf(cmd.ErrOrStderr(), "Debug: %s == %s\n", sbom1Path, sbom2Path)
212+
}
213+
return nil
214+
}
215+
216+
// SBOMs don't match
217+
fmt.Fprintf(cmd.ErrOrStderr(), "✗ SBOMs differ\n")
218+
if verifyDiff {
219+
fmt.Fprintf(cmd.OutOrStdout(), "\nDifferences:\n%s\n", diff)
220+
}
221+
222+
return fmt.Errorf("SBOMs are not reproducibly identical")
223+
}
224+
99225
func runSBOMProject(cmd *cobra.Command, args []string) error {
100226
cfg, mgr := cmdutil.SetupContext()
101227

@@ -166,9 +292,34 @@ func runSBOMProject(cmd *cobra.Command, args []string) error {
166292

167293
fmt.Fprintf(cmd.ErrOrStderr(), "goenv: SBOM written to %s\n", sbomOutput)
168294

295+
// Enhance SBOM with Go-aware metadata if enabled
296+
if sbomEnhance && (sbomTool == "cyclonedx-gomod" || sbomFormat == "cyclonedx-json") {
297+
if err := enhanceSBOM(cfg, mgr, cmd); err != nil {
298+
fmt.Fprintf(cmd.ErrOrStderr(), "goenv: Warning: Failed to enhance SBOM: %v\n", err)
299+
// Don't fail - enhancement is optional
300+
} else {
301+
fmt.Fprintf(cmd.ErrOrStderr(), "goenv: SBOM enhanced with Go-aware metadata\n")
302+
}
303+
}
304+
169305
return nil
170306
}
171307

308+
// enhanceSBOM adds Go-specific metadata to the generated SBOM
309+
func enhanceSBOM(cfg *config.Config, mgr *manager.Manager, cmd *cobra.Command) error {
310+
// Import the enhancer package
311+
enhancer := sbom.NewEnhancer(cfg, mgr)
312+
313+
opts := sbom.EnhanceOptions{
314+
ProjectDir: sbomDir,
315+
Deterministic: sbomDeterministic,
316+
OfflineMode: sbomOffline,
317+
EmbedDigests: sbomEmbedDigests || sbomDeterministic, // Always embed if deterministic
318+
}
319+
320+
return enhancer.EnhanceCycloneDX(sbomOutput, opts)
321+
}
322+
172323
// resolveSBOMTool finds the tool binary in goenv-managed paths
173324
func resolveSBOMTool(cfg *config.Config, tool string) (string, error) {
174325
// Check host-specific bin directory first using consolidated utility

0 commit comments

Comments
 (0)