@@ -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
7476var (
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
8590func 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 (), "\n Differences:\n %s\n " , diff )
220+ }
221+
222+ return fmt .Errorf ("SBOMs are not reproducibly identical" )
223+ }
224+
99225func 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
173324func resolveSBOMTool (cfg * config.Config , tool string ) (string , error ) {
174325 // Check host-specific bin directory first using consolidated utility
0 commit comments