From 355112b7a864651e71da0eb01ac2cd31d0259682 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 30 Sep 2024 14:51:08 -0400 Subject: [PATCH 01/11] Add PR suggestions --- go.mod | 5 +- go.sum | 17 ++- packer/build.go | 9 +- packer/provisioner.go | 32 ++--- provisioner/hcp_sbom/provisioner.go | 175 +++++++++++------------ provisioner/hcp_sbom/provisioner_test.go | 24 ++-- 6 files changed, 127 insertions(+), 135 deletions(-) diff --git a/go.mod b/go.mod index bd377d0f4..4e9aff68b 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/hashicorp/packer-plugin-amazon v1.2.1 github.com/hashicorp/packer-plugin-sdk v0.5.4 github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 - github.com/klauspost/compress v1.13.6 // indirect + github.com/klauspost/compress v1.13.6 github.com/klauspost/pgzip v1.2.5 github.com/masterzen/winrm v0.0.0-20210623064412-3b76017826b0 github.com/mattn/go-runewidth v0.0.13 // indirect @@ -40,7 +40,7 @@ require ( github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db // indirect github.com/pkg/sftp v1.13.2 // indirect github.com/posener/complete v1.2.3 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/ulikunitz/xz v0.5.10 github.com/zclconf/go-cty v1.13.3 github.com/zclconf/go-cty-yaml v1.0.1 @@ -58,6 +58,7 @@ require ( ) require ( + github.com/CycloneDX/cyclonedx-go v0.9.1 github.com/go-openapi/strfmt v0.21.10 github.com/oklog/ulid v1.3.1 github.com/pierrec/lz4/v4 v4.1.18 diff --git a/go.sum b/go.sum index f2f214db7..ab4d7d655 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 h1:w0E0fgc1YafGEh5cROhlROMWXiNoZqApk2PDN0M1+Ns= github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= +github.com/CycloneDX/cyclonedx-go v0.9.1 h1:yffaWOZsv77oTJa/SdVZYdgAgFioCeycBUKkqS2qzQM= +github.com/CycloneDX/cyclonedx-go v0.9.1/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -78,6 +80,8 @@ github.com/biogo/hts v1.4.3 h1:vir2yUTiRkPvtp6ZTpzh9lWTKQJZXJKZ563rpAQAsRM= github.com/biogo/hts v1.4.3/go.mod h1:eW40HJ1l2ExK9C+yvvoRSftInqWsf3ue+zAEjzCGWjA= github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= @@ -502,8 +506,9 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -515,8 +520,10 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= +github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= @@ -537,6 +544,10 @@ github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3k github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= diff --git a/packer/build.go b/packer/build.go index c1dac8944..560bcd5b5 100644 --- a/packer/build.go +++ b/packer/build.go @@ -304,12 +304,9 @@ func (b *CoreBuild) Run(ctx context.Context, originalUi packersdk.Ui) ([]packers return nil, err } - if len(b.Provisioners) > 0 { - for _, p := range b.Provisioners { - sbomInternalProvisioner, ok := p.Provisioner.(*SBOMInternalProvisioner) - if !ok { - continue - } + for _, p := range b.Provisioners { + sbomInternalProvisioner, ok := p.Provisioner.(*SBOMInternalProvisioner) + if ok { b.SBOMFilesCompressed = append(b.SBOMFilesCompressed, sbomInternalProvisioner.CompressedData) } } diff --git a/packer/provisioner.go b/packer/provisioner.go index abd8faa88..e44b48d8b 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -6,7 +6,6 @@ package packer import ( "context" "fmt" - "io" "log" "os" @@ -244,7 +243,6 @@ func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, co // press before the provisioner is actually run. type SBOMInternalProvisioner struct { Provisioner packersdk.Provisioner - TempFileLoc string CompressedData []byte } @@ -269,23 +267,28 @@ func (p *SBOMInternalProvisioner) Provision( if err != nil { return fmt.Errorf("failed to create internal temporary file for Packer SBOM: %s", err) } - defer tmpFile.Close() + + // Close the file handle before passing the name to the underlying provisioner + tmpFileName := tmpFile.Name() + if err = tmpFile.Close(); err != nil { + return fmt.Errorf("failed to close temporary file for Packer SBOM %s: %s", tmpFileName, err) + } + defer func(name string) { fileRemoveErr := os.Remove(name) if fileRemoveErr != nil { log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr) } - }(p.TempFileLoc) + }(tmpFile.Name()) generatedData["dst"] = tmpFile.Name() - p.TempFileLoc = tmpFile.Name() err = p.Provisioner.Provision(ctx, ui, comm, generatedData) if err != nil { return err } - compressedData, err := p.compressFile(p.TempFileLoc) + compressedData, err := p.compressFile(tmpFile.Name()) if err != nil { return err } @@ -294,25 +297,18 @@ func (p *SBOMInternalProvisioner) Provision( } func (p *SBOMInternalProvisioner) compressFile(filePath string) ([]byte, error) { - sourceFile, err := os.Open(filePath) + data, err := os.ReadFile(filePath) if err != nil { - return nil, err - } - defer sourceFile.Close() - - data, err := io.ReadAll(sourceFile) - if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) } - encoder, err := zstd.NewWriter(nil) + encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create zstd encoder: %w", err) } - defer encoder.Close() compressedData := encoder.EncodeAll(data, nil) - fmt.Printf(fmt.Sprintf("SBOM file compressed successfully. Size: %d bytes", len(compressedData))) + log.Printf("SBOM file compressed successfully. Size: %d bytes\n", len(compressedData)) return compressedData, nil } diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index 23984ec75..ee3127194 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -8,26 +8,40 @@ package hcp_sbom import ( "context" - "encoding/json" "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" + "fmt" + "log" + "os" + + "github.com/CycloneDX/cyclonedx-go" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/packer-plugin-sdk/common" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" + + "path/filepath" ) type Config struct { common.PackerConfig `mapstructure:",squash"` - Source string `mapstructure:"source" required:"true"` - Destination string `mapstructure:"destination"` - ctx interpolate.Context + + // Source is a required field that specifies the path to the SBOM file that + // needs to be downloaded. + // It can be a file path or a URL. + Source string `mapstructure:"source" required:"true"` + // Destination is an optional field that specifies the path where the SBOM + // file will be downloaded to for the user. + // The 'Destination' must be a writable location. If the destination is a file, + // the SBOM will be saved or overwritten at that path. If the destination is + // a directory, a file will be created within the directory to store the SBOM. + // Any parent directories for the destination must already exist and be + // writable by the provisioning user (generally not root), otherwise, + // a "Permission Denied" error will occur. If the source path is a file, + // it is recommended that the destination path be a file as well. + Destination string `mapstructure:"destination"` + ctx interpolate.Context } type Provisioner struct { @@ -67,7 +81,7 @@ func (p *Provisioner) Provision( ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, ) error { - ui.Say( + log.Printf( fmt.Sprintf("Starting to provision with hcp-sbom using source: %s", p.config.Source, ), @@ -81,14 +95,17 @@ func (p *Provisioner) Provision( // Download the file for Packer destPath, downloadErr := p.downloadSBOMForPacker(ui, comm, generatedData) if downloadErr != nil { - return fmt.Errorf("failed to download file: %w", downloadErr) + return fmt.Errorf("failed to download Packer SBOM file: %w", downloadErr) } // Download the file for user - p.downloadSBOMForUser(ui, comm) + downloadErr = p.downloadSBOMForUser(ui, comm) + if downloadErr != nil { + return fmt.Errorf("failed to download User SBOM file: %w", downloadErr) + } // Validate the file - ui.Say(fmt.Sprintf("Validating SBOM file %s", destPath)) + log.Printf(fmt.Sprintf("Validating SBOM file: %s\n", destPath)) validationErr := p.validateSBOM(ui, destPath) if validationErr != nil { return fmt.Errorf("failed to validate SBOM file: %w", validationErr) @@ -108,40 +125,27 @@ func (p *Provisioner) downloadSBOMForPacker( return p.config.Destination, fmt.Errorf("error interpolating source: %s", err) } - // FIXME:: Do we really need this? - // Check if the source is a JSON file - if filepath.Ext(src) != ".json" { - return p.config.Destination, fmt.Errorf( - "packer SBOM source file is not a JSON file: %s", src, - ) - } - // Download the file for Packer - desti, ok := generatedData["dst"] // this has been set by HCPSBOMInternalProvisioner.Provision - if !ok { - return "", fmt.Errorf("failed to find location for Packer SBOM file") + dst, ok := generatedData["dst"].(string) // this has been set by HCPSBOMInternalProvisioner.Provision + if !ok || dst == "" { + return "", fmt.Errorf("destination path for Packer SBOM file is not valid") } - dst := fmt.Sprintf("%v", desti) // Ensure the destination directory exists - dir := filepath.Dir(dst) - if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { - return dst, fmt.Errorf("failed to create destination directory for Packer SBOM: %s", err) + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return dst, fmt.Errorf("failed to create destination directory for Packer SBOM: %w", err) } // Open the destination file f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - return dst, fmt.Errorf("failed to open destination file: %s", err) + return dst, fmt.Errorf("failed to open destination file for Packer SBOM: %s", err) } defer f.Close() - // Create MultiWriter for the current progress - pf := io.MultiWriter(f) - // Download the file ui.Say(fmt.Sprintf("Downloading SBOM file %s for Packer => %s", src, dst)) - if err = comm.Download(src, pf); err != nil { + if err = comm.Download(src, f); err != nil { ui.Error(fmt.Sprintf("download failed for Packer SBOM file: %s", err)) return dst, err } @@ -149,101 +153,84 @@ func (p *Provisioner) downloadSBOMForPacker( return dst, nil } -// downloadSBOMForUser downloads a SBOM from a specified source to a local -// destination given by user. It works with all communicators from packersdk. +// downloadSBOMForUser downloads a Software Bill of Materials (SBOM) file from a specified source +// to a local destination path on the machine. func (p *Provisioner) downloadSBOMForUser( ui packersdk.Ui, comm packersdk.Communicator, -) { - src, err := interpolate.Render(p.config.Source, &p.config.ctx) - if err != nil { - ui.Say(fmt.Sprintf("error interpolating source: %s", err)) - return - } - - // Determine the destination path +) error { dst := p.config.Destination if dst == "" { - ui.Say("skipped downloading SBOM file for user because 'Destination' is not provided") - return + log.Println("skipped downloading user SBOM file because 'Destination' is not provided") + return nil } - dst, err = interpolate.Render(dst, &p.config.ctx) + dst, err := interpolate.Render(dst, &p.config.ctx) if err != nil { - ui.Say(fmt.Sprintf("error interpolating SBOM file destination: %s", err)) - return + return fmt.Errorf("error interpolating SBOM file destination from user: %s\n", err) } - if strings.HasSuffix(dst, "/") { - info, err := os.Stat(dst) - if err != nil { - ui.Say(fmt.Sprintf("failed to stat destination for SBOM: %s", err)) - return - } + src, err := interpolate.Render(p.config.Source, &p.config.ctx) + if err != nil { + return fmt.Errorf("error interpolating source: %s", err) + } - if info.IsDir() { - tmpFile, err := os.CreateTemp(dst, "packer-user-sbom-*.json") - if err != nil { - ui.Say(fmt.Sprintf("failed to create file for Packer SBOM: %s", err)) - return + // Check if the destination exists and determine its type + info, err := os.Stat(dst) + if err != nil { + if os.IsNotExist(err) { + // If destination doesn't exist, assume it's a file path and ensure parent directories are created + dir := filepath.Dir(dst) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create destination directory for user SBOM: %s\n", err) } - dst = tmpFile.Name() - tmpFile.Close() + } else { + return fmt.Errorf("failed to stat destination for user SBOM: %s\n", err) } - } - - // Ensure the destination directory exists - dir := filepath.Dir(dst) - if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { - ui.Say(fmt.Sprintf("failed to create destination directory for Packer SBOM: %s", err)) - return + } else if info.IsDir() { + // If the destination is a directory, create a temporary file inside it + tmpFile, err := os.CreateTemp(dst, "packer-user-sbom-*.json") + if err != nil { + return fmt.Errorf("failed to create temporary file in user SBOM directory %s: %s", dst, err) + } + dst = tmpFile.Name() + tmpFile.Close() } // Open the destination file f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - ui.Say(fmt.Sprintf("failed to open destination file: %s", err)) - return + return fmt.Errorf("failed to open destination file for user SBOM: %s", err) } defer f.Close() - // Create MultiWriter for the current progress - pf := io.MultiWriter(f) - // Download the file ui.Say(fmt.Sprintf("Downloading SBOM file for user %s => %s", src, dst)) - if err = comm.Download(src, pf); err != nil { - ui.Error(fmt.Sprintf("download failed for user SBOM file: %s", err)) - return + if err = comm.Download(src, f); err != nil { + return fmt.Errorf("download failed for user SBOM file: %s", err) } + + ui.Say(fmt.Sprintf("User SBOM file successfully downloaded to: %s\n", dst)) + return nil } -type SBOM struct { - BomFormat string `json:"bomFormat"` - SpecVersion string `json:"specVersion"` -} - +// validateSBOM validates CycloneDX SBOM files func (p *Provisioner) validateSBOM(ui packersdk.Ui, filePath string) error { sourceFile, err := os.Open(filePath) if err != nil { - return err + return fmt.Errorf("failed to open file %s: %w", filePath, err) } defer sourceFile.Close() - data, err := io.ReadAll(sourceFile) - if err != nil { - return err + decoder := cyclonedx.NewBOMDecoder(sourceFile, cyclonedx.BOMFileFormatJSON) + bom := new(cyclonedx.BOM) + if err := decoder.Decode(bom); err != nil { + return fmt.Errorf("failed to decode CycloneDX SBOM: %w", err) } - var sbom SBOM - if err := json.Unmarshal(data, &sbom); err != nil { - return fmt.Errorf("failed to unmarshal JSON: %w", err) + if bom.BOMFormat != "CycloneDX" { + return fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat) } - - if sbom.BomFormat != "CycloneDX" { - return fmt.Errorf("invalid bomFormat: %s", sbom.BomFormat) - } - - if sbom.SpecVersion == "" { + if bom.SpecVersion.String() == "" { return fmt.Errorf("specVersion is required") } diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go index ef4475072..8509bea70 100644 --- a/provisioner/hcp_sbom/provisioner_test.go +++ b/provisioner/hcp_sbom/provisioner_test.go @@ -136,35 +136,35 @@ func TestValidateSBOM(t *testing.T) { tests := []struct { name string - sbom SBOM + sbom map[string]interface{} expectError bool errorMsg string }{ { name: "Valid SBOM", - sbom: SBOM{ - BomFormat: "CycloneDX", - SpecVersion: "1.0", + sbom: map[string]interface{}{ + "bomFormat": "CycloneDX", + "specVersion": "1.0", }, expectError: false, }, { name: "Invalid BomFormat", - sbom: SBOM{ - BomFormat: "InvalidFormat", - SpecVersion: "1.0", + sbom: map[string]interface{}{ + "bomFormat": "InvalidFormat", + "specVersion": "1.0", }, expectError: true, - errorMsg: "invalid bomFormat: InvalidFormat", + errorMsg: "invalid bomFormat: InvalidFormat, expected CycloneDX", }, { name: "Empty SpecVersion", - sbom: SBOM{ - BomFormat: "CycloneDX", - SpecVersion: "", + sbom: map[string]interface{}{ + "bomFormat": "CycloneDX", + "specVersion": "", }, expectError: true, - errorMsg: "specVersion is required", + errorMsg: "failed to decode CycloneDX SBOM: invalid specification version", }, } From 498fd1dfabe7cb3a0c426b3523f49c856852fec2 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 30 Sep 2024 15:00:11 -0400 Subject: [PATCH 02/11] Remove unnecessary test --- provisioner/hcp_sbom/provisioner_test.go | 109 ----------------------- 1 file changed, 109 deletions(-) diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go index 8509bea70..88ff6d8f6 100644 --- a/provisioner/hcp_sbom/provisioner_test.go +++ b/provisioner/hcp_sbom/provisioner_test.go @@ -3,7 +3,6 @@ package hcp_sbom import ( "encoding/json" "fmt" - "io" "os" "testing" @@ -22,114 +21,6 @@ func (m *MockUi) Error(message string) { fmt.Println("ERROR:", message) } -type MockCommunicator struct { - packer.Communicator -} - -func (m *MockCommunicator) Download(src string, dst io.Writer) error { - _, err := dst.Write([]byte("mock SBOM content")) - return err -} - -func TestDownloadSBOMForPacker(t *testing.T) { - ui := &MockUi{} - comm := &MockCommunicator{} - - tests := []struct { - name string - config Config - expectError bool - }{ - { - name: "Source is a dir, Dest is a dir", - config: Config{ - Source: "mock-source/", - Destination: "test-dir/", - }, - expectError: true, - }, - { - name: "Source is a json file, Destination is a dir", - config: Config{ - Source: "mock-source/sbom.json", - Destination: "test-dir/", - }, - expectError: false, - }, - { - name: "Source is a json file, Destination is a json file", - config: Config{ - Source: "mock-source/sbom.json", - Destination: "sbom.json", - }, - expectError: false, - }, - { - name: "Source is a json file, Destination is a json file in test-output-data", - config: Config{ - Source: "mock-source/sbom.json", - Destination: "test-output-data/sbom.json", - }, - expectError: false, - }, - { - name: "Source is a json file, Destination is test-output-data w/o /", - config: Config{ - Source: "mock-source/sbom.json", - Destination: "test-output-data", - }, - expectError: false, - }, - { - name: "Source is a json file, Destination is empty", - config: Config{ - Source: "mock-source/sbom.json", - }, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - provisioner := &Provisioner{ - config: tt.config, - } - - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("failed to get current working directory for Packer SBOM: %s", err) - } - - tmpFile, err := os.CreateTemp(cwd, "packer-sbom-*.json") - if err != nil { - t.Fatalf("failed to create internal temporary file for Packer SBOM: %s", err) - } - generatedData := map[string]interface{}{ - "dst": tmpFile.Name(), - } - defer tmpFile.Close() - defer os.Remove(tmpFile.Name()) - - destPath, err := provisioner.downloadSBOMForPacker(ui, comm, generatedData) - if tt.expectError { - if err == nil { - t.Fatalf("expected error, got none") - } - } else { - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if _, err := os.Stat(destPath); os.IsNotExist(err) { - t.Fatalf("expected file to exist at %s", destPath) - } - - os.RemoveAll(destPath) - } - }) - } -} - func TestValidateSBOM(t *testing.T) { provisioner := &Provisioner{} ui := &MockUi{} From 647056a7757e214a4c460e9cbc603fe0dfe5c30e Mon Sep 17 00:00:00 2001 From: Devashish Date: Fri, 4 Oct 2024 14:01:57 -0400 Subject: [PATCH 03/11] Run generate --- .../provisioner/hcp_sbom/Config-not-required.mdx | 10 +++++++++- .../partials/provisioner/hcp_sbom/Config-required.mdx | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx b/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx index a8019fbde..a46cec04b 100644 --- a/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx +++ b/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx @@ -1,5 +1,13 @@ -- `destination` (string) - Destination +- `destination` (string) - Destination is an optional field that specifies the path where the SBOM + file will be downloaded to for the user. + The 'Destination' must be a writable location. If the destination is a file, + the SBOM will be saved or overwritten at that path. If the destination is + a directory, a file will be created within the directory to store the SBOM. + Any parent directories for the destination must already exist and be + writable by the provisioning user (generally not root), otherwise, + a "Permission Denied" error will occur. If the source path is a file, + it is recommended that the destination path be a file as well. diff --git a/website/content/partials/provisioner/hcp_sbom/Config-required.mdx b/website/content/partials/provisioner/hcp_sbom/Config-required.mdx index 0cb7e7a80..936c435f6 100644 --- a/website/content/partials/provisioner/hcp_sbom/Config-required.mdx +++ b/website/content/partials/provisioner/hcp_sbom/Config-required.mdx @@ -1,5 +1,7 @@ -- `source` (string) - Source +- `source` (string) - Source is a required field that specifies the path to the SBOM file that + needs to be downloaded. + It can be a file path or a URL. From b0589f53e06703687b4c40086c5d672692045e86 Mon Sep 17 00:00:00 2001 From: Devashish Date: Tue, 8 Oct 2024 21:56:50 -0400 Subject: [PATCH 04/11] DRY download SBOM functions --- provisioner/hcp_sbom/provisioner.go | 118 +++++++++++++++------------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index ee3127194..db906c83a 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -92,16 +92,10 @@ func (p *Provisioner) Provision( } p.config.ctx.Data = generatedData - // Download the file for Packer - destPath, downloadErr := p.downloadSBOMForPacker(ui, comm, generatedData) + // Download the files + destPath, downloadErr := p.downloadSBOM(ui, comm, generatedData) if downloadErr != nil { - return fmt.Errorf("failed to download Packer SBOM file: %w", downloadErr) - } - - // Download the file for user - downloadErr = p.downloadSBOMForUser(ui, comm) - if downloadErr != nil { - return fmt.Errorf("failed to download User SBOM file: %w", downloadErr) + return fmt.Errorf("failed to download SBOM file: %w", downloadErr) } // Validate the file @@ -114,64 +108,58 @@ func (p *Provisioner) Provision( return nil } -// downloadSBOMForPacker downloads SBOM from a specified source to a local -// destination set by internal SBOM provisioner. It works with all communicators -// from packersdk. -func (p *Provisioner) downloadSBOMForPacker( +// downloadSBOM handles downloading SBOM files for the User and Packer. +func (p *Provisioner) downloadSBOM( ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, ) (string, error) { + // Interpolate the source path src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { - return p.config.Destination, fmt.Errorf("error interpolating source: %s", err) + return "", fmt.Errorf("error interpolating source: %s", err) } - // Download the file for Packer - dst, ok := generatedData["dst"].(string) // this has been set by HCPSBOMInternalProvisioner.Provision - if !ok || dst == "" { - return "", fmt.Errorf("destination path for Packer SBOM file is not valid") - } - - // Ensure the destination directory exists - if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { - return dst, fmt.Errorf("failed to create destination directory for Packer SBOM: %w", err) - } - - // Open the destination file - f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + // Attempt to download SBOM for User + dst, err := p.getUserDestination() if err != nil { - return dst, fmt.Errorf("failed to open destination file for Packer SBOM: %s", err) - } - defer f.Close() - - // Download the file - ui.Say(fmt.Sprintf("Downloading SBOM file %s for Packer => %s", src, dst)) - if err = comm.Download(src, f); err != nil { - ui.Error(fmt.Sprintf("download failed for Packer SBOM file: %s", err)) - return dst, err + return "", fmt.Errorf("failed to determine user SBOM destination: %s", err) } + // If User SBOM destination is valid, try to download the SBOM file + if dst != "" { + ui.Say(fmt.Sprintf("Attempting to download SBOM file for User: %s", src)) + err = p.downloadToFile(ui, comm, src, dst) + if err != nil { + return "", fmt.Errorf("user SBOM download failed: %s", err) + } + ui.Say(fmt.Sprintf("User SBOM file successfully downloaded to: %s", dst)) + } + + // Attempt to download SBOM for Packer + dst, err = p.getPackerDestination(generatedData) + if err != nil { + return "", fmt.Errorf("failed to get Packer SBOM destination: %s", err) + } + + err = p.downloadToFile(ui, comm, src, dst) + if err != nil { + return "", fmt.Errorf("failed to download Packer SBOM: %s", err) + } + + ui.Say(fmt.Sprintf("Packer SBOM file successfully downloaded to: %s", dst)) return dst, nil } -// downloadSBOMForUser downloads a Software Bill of Materials (SBOM) file from a specified source -// to a local destination path on the machine. -func (p *Provisioner) downloadSBOMForUser( - ui packersdk.Ui, comm packersdk.Communicator, -) error { +// getUserDestination determines and returns the destination path for the user SBOM file. +func (p *Provisioner) getUserDestination() (string, error) { dst := p.config.Destination if dst == "" { log.Println("skipped downloading user SBOM file because 'Destination' is not provided") - return nil + return "", nil } dst, err := interpolate.Render(dst, &p.config.ctx) if err != nil { - return fmt.Errorf("error interpolating SBOM file destination from user: %s\n", err) - } - - src, err := interpolate.Render(p.config.Source, &p.config.ctx) - if err != nil { - return fmt.Errorf("error interpolating source: %s", err) + return "", fmt.Errorf("error interpolating SBOM file destination for user: %s", err) } // Check if the destination exists and determine its type @@ -181,35 +169,55 @@ func (p *Provisioner) downloadSBOMForUser( // If destination doesn't exist, assume it's a file path and ensure parent directories are created dir := filepath.Dir(dst) if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create destination directory for user SBOM: %s\n", err) + return "", fmt.Errorf("failed to create destination directory for user SBOM: %s\n", err) } } else { - return fmt.Errorf("failed to stat destination for user SBOM: %s\n", err) + return "", fmt.Errorf("failed to stat destination for user SBOM: %s\n", err) } } else if info.IsDir() { // If the destination is a directory, create a temporary file inside it tmpFile, err := os.CreateTemp(dst, "packer-user-sbom-*.json") if err != nil { - return fmt.Errorf("failed to create temporary file in user SBOM directory %s: %s", dst, err) + return "", fmt.Errorf("failed to create temporary file in user SBOM directory %s: %s", dst, err) } dst = tmpFile.Name() tmpFile.Close() } + return dst, nil +} + +// getPackerDestination retrieves the destination path for the Packer SBOM file. +func (p *Provisioner) getPackerDestination(generatedData map[string]interface{}) (string, error) { + dst, ok := generatedData["dst"].(string) // This has been set by HCPSBOMInternalProvisioner.Provision + if !ok || dst == "" { + return "", fmt.Errorf("destination path for Packer SBOM file is not valid") + } + + // Ensure the destination directory exists + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return "", fmt.Errorf("failed to create destination directory for Packer SBOM: %w", err) + } + + return dst, nil +} + +// downloadToFile performs the actual download operation to the specified file destination. +func (p *Provisioner) downloadToFile(ui packersdk.Ui, comm packersdk.Communicator, src, dst string) error { // Open the destination file f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - return fmt.Errorf("failed to open destination file for user SBOM: %s", err) + return fmt.Errorf("failed to open destination file for SBOM: %s", err) } defer f.Close() // Download the file - ui.Say(fmt.Sprintf("Downloading SBOM file for user %s => %s", src, dst)) + ui.Say(fmt.Sprintf("Downloading SBOM file %s => %s", src, dst)) if err = comm.Download(src, f); err != nil { - return fmt.Errorf("download failed for user SBOM file: %s", err) + ui.Error(fmt.Sprintf("download failed for SBOM file: %s", err)) + return err } - ui.Say(fmt.Sprintf("User SBOM file successfully downloaded to: %s\n", dst)) return nil } From 17ee3e9b7f8651255d7b0e3eef600ec4467dd163 Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 9 Oct 2024 10:25:30 -0400 Subject: [PATCH 05/11] Add support for SPDX --- go.mod | 2 + go.sum | 7 ++ packer/build.go | 17 ++- packer/provisioner.go | 127 +++++++++++++++++++++++ provisioner/hcp_sbom/provisioner.go | 52 ++-------- provisioner/hcp_sbom/provisioner_test.go | 81 --------------- 6 files changed, 158 insertions(+), 128 deletions(-) delete mode 100644 provisioner/hcp_sbom/provisioner_test.go diff --git a/go.mod b/go.mod index 4e9aff68b..a5072ae8d 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( github.com/oklog/ulid v1.3.1 github.com/pierrec/lz4/v4 v4.1.18 github.com/shirou/gopsutil/v3 v3.23.4 + github.com/spdx/tools-golang v0.5.5 ) require ( @@ -79,6 +80,7 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/agext/levenshtein v1.2.3 // indirect + github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect github.com/apparentlymart/go-cidr v1.0.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect diff --git a/go.sum b/go.sum index ab4d7d655..a7a5c4433 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antchfx/xmlquery v1.3.5 h1:I7TuBRqsnfFuL11ruavGm911Awx9IqSdiU6W/ztSmVw= @@ -501,6 +503,9 @@ github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2 github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= +github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= +github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -520,6 +525,7 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= @@ -769,3 +775,4 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/packer/build.go b/packer/build.go index 560bcd5b5..313fb0d38 100644 --- a/packer/build.go +++ b/packer/build.go @@ -51,13 +51,18 @@ type CoreBuild struct { l sync.Mutex prepareCalled bool - SBOMFilesCompressed [][]byte + SBOMs []SBOM +} + +type SBOM struct { + Format string + CompressedData []byte } type BuildMetadata struct { PackerVersion string Plugins map[string]PluginDetails - SBOMs [][]byte + SBOMs []SBOM } func (b *CoreBuild) getPluginsMetadata() map[string]PluginDetails { @@ -91,7 +96,7 @@ func (b *CoreBuild) GetMetadata() BuildMetadata { metadata := BuildMetadata{ PackerVersion: version.FormattedVersion(), Plugins: b.getPluginsMetadata(), - SBOMs: b.SBOMFilesCompressed, + SBOMs: b.SBOMs, } return metadata } @@ -307,7 +312,11 @@ func (b *CoreBuild) Run(ctx context.Context, originalUi packersdk.Ui) ([]packers for _, p := range b.Provisioners { sbomInternalProvisioner, ok := p.Provisioner.(*SBOMInternalProvisioner) if ok { - b.SBOMFilesCompressed = append(b.SBOMFilesCompressed, sbomInternalProvisioner.CompressedData) + sbom := SBOM{ + Format: string(sbomInternalProvisioner.SBOMFormat), + CompressedData: sbomInternalProvisioner.CompressedData, + } + b.SBOMs = append(b.SBOMs, sbom) } } diff --git a/packer/provisioner.go b/packer/provisioner.go index e44b48d8b..7e15ee80e 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -6,8 +6,11 @@ package packer import ( "context" "fmt" + "github.com/CycloneDX/cyclonedx-go" + spdxjson "github.com/spdx/tools-golang/json" "log" "os" + "strings" "github.com/klauspost/compress/zstd" @@ -244,6 +247,7 @@ func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, co type SBOMInternalProvisioner struct { Provisioner packersdk.Provisioner CompressedData []byte + SBOMFormat SBOMFormat } func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() } @@ -288,11 +292,17 @@ func (p *SBOMInternalProvisioner) Provision( return err } + format, err := p.validateSBOM(tmpFile.Name()) + if err != nil { + return err + } + compressedData, err := p.compressFile(tmpFile.Name()) if err != nil { return err } p.CompressedData = compressedData + p.SBOMFormat = format return nil } @@ -312,3 +322,120 @@ func (p *SBOMInternalProvisioner) compressFile(filePath string) ([]byte, error) log.Printf("SBOM file compressed successfully. Size: %d bytes\n", len(compressedData)) return compressedData, nil } + +type SBOMFormat string + +const ( + CycloneDX SBOMFormat = "CycloneDX" + SPDX SBOMFormat = "SPDX" +) + +// SBOMValidator defines the interface for SBOM validation. +type SBOMValidator interface { + Validate(file *os.File) error +} + +// CycloneDxValidator validates CycloneDx SBOM files. +type CycloneDxValidator struct{} + +// Validate performs validation for CycloneDX files. +func (v *CycloneDxValidator) Validate(file *os.File) error { + decoder := cyclonedx.NewBOMDecoder(file, cyclonedx.BOMFileFormatJSON) + bom := new(cyclonedx.BOM) + if err := decoder.Decode(bom); err != nil { + return fmt.Errorf("failed to decode CycloneDX SBOM: %w", err) + } + + if bom.BOMFormat != "CycloneDX" { + return fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat) + } + if bom.SpecVersion.String() == "" { + return fmt.Errorf("specVersion is required") + } + + return nil +} + +// SPDXValidator validates SPDX SBOM files. +type SPDXValidator struct{} + +// Validate performs validation for SPDX files in JSON format. +func (v *SPDXValidator) Validate(file *os.File) error { + doc, err := spdxjson.Read(file) + if err != nil { + return fmt.Errorf("error parsing SPDX JSON file: %w", err) + } + + if doc.SPDXVersion == "" { + return fmt.Errorf("SPDX validation error: missing SPDXVersion") + } + + return nil +} + +// detectSBOMFormat reads the file and detects whether it is a CycloneDX or SPDX file. +func detectSBOMFormat(file *os.File) (SBOMFormat, error) { + // Read a few bytes of the file to determine its type + buffer := make([]byte, 512) + if _, err := file.Read(buffer); err != nil { + return "", fmt.Errorf("failed to read SBOM file: %w", err) + } + + if strings.Contains(string(buffer), "CycloneDX") { + return CycloneDX, nil + } + + if strings.Contains(string(buffer), "SPDX-") { + return SPDX, nil + } + + return "", fmt.Errorf("unsupported or unknown SBOM format") +} + +// NewSBOMValidator is a factory function that returns the appropriate validator based on the file format. +func NewSBOMValidator(format SBOMFormat) (SBOMValidator, error) { + switch format { + case CycloneDX: + return &CycloneDxValidator{}, nil + case SPDX: + return &SPDXValidator{}, nil + default: + return nil, fmt.Errorf("unsupported SBOM format: %s", format) + } +} + +// validateSBOM validates the SBOM file against supported formats (CycloneDx, SPDX). +func (p *SBOMInternalProvisioner) validateSBOM(filePath string) (SBOMFormat, error) { + // Open the SBOM file for reading + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("failed to open SBOM file %s: %w", filePath, err) + } + defer file.Close() + + // Detect the format of the SBOM + format, err := detectSBOMFormat(file) + if err != nil { + return "", fmt.Errorf("failed to detect SBOM format: %w", err) + } + + // Create the appropriate validator + validator, err := NewSBOMValidator(format) + if err != nil { + return "", err + } + + // Seek back to the beginning of the file for validation + if _, err := file.Seek(0, 0); err != nil { + return "", fmt.Errorf("failed to seek SBOM file: %w", err) + } + + // Perform validation using the selected validator + err = validator.Validate(file) + if err != nil { + return "", fmt.Errorf("validation failed for %s format: %w", format, err) + } + + log.Printf(fmt.Sprintf("SBOM file %s is valid for format: %s", filePath, format)) + return format, nil +} diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index db906c83a..0c6d9925d 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -9,18 +9,15 @@ package hcp_sbom import ( "context" "errors" - "fmt" "log" "os" - "github.com/CycloneDX/cyclonedx-go" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/packer-plugin-sdk/common" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" - "path/filepath" ) @@ -93,35 +90,28 @@ func (p *Provisioner) Provision( p.config.ctx.Data = generatedData // Download the files - destPath, downloadErr := p.downloadSBOM(ui, comm, generatedData) + downloadErr := p.downloadSBOM(ui, comm, generatedData) if downloadErr != nil { return fmt.Errorf("failed to download SBOM file: %w", downloadErr) } - // Validate the file - log.Printf(fmt.Sprintf("Validating SBOM file: %s\n", destPath)) - validationErr := p.validateSBOM(ui, destPath) - if validationErr != nil { - return fmt.Errorf("failed to validate SBOM file: %w", validationErr) - } - return nil } // downloadSBOM handles downloading SBOM files for the User and Packer. func (p *Provisioner) downloadSBOM( ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, -) (string, error) { +) error { // Interpolate the source path src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { - return "", fmt.Errorf("error interpolating source: %s", err) + return fmt.Errorf("error interpolating source: %s", err) } // Attempt to download SBOM for User dst, err := p.getUserDestination() if err != nil { - return "", fmt.Errorf("failed to determine user SBOM destination: %s", err) + return fmt.Errorf("failed to determine user SBOM destination: %s", err) } // If User SBOM destination is valid, try to download the SBOM file @@ -129,7 +119,7 @@ func (p *Provisioner) downloadSBOM( ui.Say(fmt.Sprintf("Attempting to download SBOM file for User: %s", src)) err = p.downloadToFile(ui, comm, src, dst) if err != nil { - return "", fmt.Errorf("user SBOM download failed: %s", err) + return fmt.Errorf("user SBOM download failed: %s", err) } ui.Say(fmt.Sprintf("User SBOM file successfully downloaded to: %s", dst)) } @@ -137,16 +127,16 @@ func (p *Provisioner) downloadSBOM( // Attempt to download SBOM for Packer dst, err = p.getPackerDestination(generatedData) if err != nil { - return "", fmt.Errorf("failed to get Packer SBOM destination: %s", err) + return fmt.Errorf("failed to get Packer SBOM destination: %s", err) } err = p.downloadToFile(ui, comm, src, dst) if err != nil { - return "", fmt.Errorf("failed to download Packer SBOM: %s", err) + return fmt.Errorf("failed to download Packer SBOM: %s", err) } ui.Say(fmt.Sprintf("Packer SBOM file successfully downloaded to: %s", dst)) - return dst, nil + return nil } // getUserDestination determines and returns the destination path for the user SBOM file. @@ -219,28 +209,4 @@ func (p *Provisioner) downloadToFile(ui packersdk.Ui, comm packersdk.Communicato } return nil -} - -// validateSBOM validates CycloneDX SBOM files -func (p *Provisioner) validateSBOM(ui packersdk.Ui, filePath string) error { - sourceFile, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open file %s: %w", filePath, err) - } - defer sourceFile.Close() - - decoder := cyclonedx.NewBOMDecoder(sourceFile, cyclonedx.BOMFileFormatJSON) - bom := new(cyclonedx.BOM) - if err := decoder.Decode(bom); err != nil { - return fmt.Errorf("failed to decode CycloneDX SBOM: %w", err) - } - - if bom.BOMFormat != "CycloneDX" { - return fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat) - } - if bom.SpecVersion.String() == "" { - return fmt.Errorf("specVersion is required") - } - - return nil -} +} \ No newline at end of file diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go deleted file mode 100644 index 88ff6d8f6..000000000 --- a/provisioner/hcp_sbom/provisioner_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package hcp_sbom - -import ( - "encoding/json" - "fmt" - "os" - "testing" - - "github.com/hashicorp/packer-plugin-sdk/packer" -) - -type MockUi struct { - packer.Ui -} - -func (m *MockUi) Say(message string) { - fmt.Println(message) -} - -func (m *MockUi) Error(message string) { - fmt.Println("ERROR:", message) -} - -func TestValidateSBOM(t *testing.T) { - provisioner := &Provisioner{} - ui := &MockUi{} - - tests := []struct { - name string - sbom map[string]interface{} - expectError bool - errorMsg string - }{ - { - name: "Valid SBOM", - sbom: map[string]interface{}{ - "bomFormat": "CycloneDX", - "specVersion": "1.0", - }, - expectError: false, - }, - { - name: "Invalid BomFormat", - sbom: map[string]interface{}{ - "bomFormat": "InvalidFormat", - "specVersion": "1.0", - }, - expectError: true, - errorMsg: "invalid bomFormat: InvalidFormat, expected CycloneDX", - }, - { - name: "Empty SpecVersion", - sbom: map[string]interface{}{ - "bomFormat": "CycloneDX", - "specVersion": "", - }, - expectError: true, - errorMsg: "failed to decode CycloneDX SBOM: invalid specification version", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data, _ := json.Marshal(tt.sbom) - filePath := "test-sbom.json" - os.WriteFile(filePath, data, 0644) - defer os.Remove(filePath) - - err := provisioner.validateSBOM(ui, filePath) - if tt.expectError { - if err == nil || err.Error() != tt.errorMsg { - t.Fatalf("expected error %v, got %v", tt.errorMsg, err) - } - } else { - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - } - }) - } -} From 00b8730e17b8cfd525eaaa691da2137202fca9fd Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 9 Oct 2024 10:29:46 -0400 Subject: [PATCH 06/11] Fix linting --- packer/build.go | 4 ++-- packer/provisioner.go | 7 ++++--- provisioner/hcp_sbom/provisioner.go | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packer/build.go b/packer/build.go index 313fb0d38..e8d8e8651 100644 --- a/packer/build.go +++ b/packer/build.go @@ -62,7 +62,7 @@ type SBOM struct { type BuildMetadata struct { PackerVersion string Plugins map[string]PluginDetails - SBOMs []SBOM + SBOMs []SBOM } func (b *CoreBuild) getPluginsMetadata() map[string]PluginDetails { @@ -96,7 +96,7 @@ func (b *CoreBuild) GetMetadata() BuildMetadata { metadata := BuildMetadata{ PackerVersion: version.FormattedVersion(), Plugins: b.getPluginsMetadata(), - SBOMs: b.SBOMs, + SBOMs: b.SBOMs, } return metadata } diff --git a/packer/provisioner.go b/packer/provisioner.go index 7e15ee80e..df63a9ec8 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -6,12 +6,13 @@ package packer import ( "context" "fmt" - "github.com/CycloneDX/cyclonedx-go" - spdxjson "github.com/spdx/tools-golang/json" "log" "os" "strings" + "github.com/CycloneDX/cyclonedx-go" + spdxjson "github.com/spdx/tools-golang/json" + "github.com/klauspost/compress/zstd" "time" @@ -247,7 +248,7 @@ func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, co type SBOMInternalProvisioner struct { Provisioner packersdk.Provisioner CompressedData []byte - SBOMFormat SBOMFormat + SBOMFormat SBOMFormat } func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() } diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index 0c6d9925d..5b48c58cd 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -13,12 +13,13 @@ import ( "log" "os" + "path/filepath" + "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/packer-plugin-sdk/common" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" - "path/filepath" ) type Config struct { @@ -209,4 +210,4 @@ func (p *Provisioner) downloadToFile(ui packersdk.Ui, comm packersdk.Communicato } return nil -} \ No newline at end of file +} From f2161a6c9cb79515f0bd0c3fa29ec7d8e24c2296 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 14 Oct 2024 17:18:05 -0400 Subject: [PATCH 07/11] Optimize code --- packer/build.go | 2 +- packer/provisioner.go | 127 +++------------------------- provisioner/hcp_sbom/provisioner.go | 66 ++++++++------- provisioner/hcp_sbom/validate.go | 76 +++++++++++++++++ 4 files changed, 122 insertions(+), 149 deletions(-) create mode 100644 provisioner/hcp_sbom/validate.go diff --git a/packer/build.go b/packer/build.go index e8d8e8651..eade2625d 100644 --- a/packer/build.go +++ b/packer/build.go @@ -313,7 +313,7 @@ func (b *CoreBuild) Run(ctx context.Context, originalUi packersdk.Ui) ([]packers sbomInternalProvisioner, ok := p.Provisioner.(*SBOMInternalProvisioner) if ok { sbom := SBOM{ - Format: string(sbomInternalProvisioner.SBOMFormat), + Format: sbomInternalProvisioner.SBOMFormat, CompressedData: sbomInternalProvisioner.CompressedData, } b.SBOMs = append(b.SBOMs, sbom) diff --git a/packer/provisioner.go b/packer/provisioner.go index df63a9ec8..fcea43a64 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -8,10 +8,8 @@ import ( "fmt" "log" "os" - "strings" - "github.com/CycloneDX/cyclonedx-go" - spdxjson "github.com/spdx/tools-golang/json" + hcpSbomProvisioner "github.com/hashicorp/packer/provisioner/hcp_sbom" "github.com/klauspost/compress/zstd" @@ -243,12 +241,14 @@ func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, co return p.Provisioner.Provision(ctx, ui, comm, generatedData) } -// SBOMInternalProvisioner is a Provisioner implementation that waits until a key -// press before the provisioner is actually run. +// SBOMInternalProvisioner is a wrapper provisioner for the `hcp_sbom` provisioner +// that sets the path for SBOM file download and, after the successful execution of +// the `hcp_sbom` provisioner, compresses the SBOM and prepares the data for API +// integration. type SBOMInternalProvisioner struct { Provisioner packersdk.Provisioner CompressedData []byte - SBOMFormat SBOMFormat + SBOMFormat string } func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() } @@ -261,19 +261,16 @@ func (p *SBOMInternalProvisioner) Provision( ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, ) error { - // Get the current working directory cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current working directory for Packer SBOM: %s", err) } - // Create a temporary file in the current working directory tmpFile, err := os.CreateTemp(cwd, "packer-sbom-*.json") if err != nil { return fmt.Errorf("failed to create internal temporary file for Packer SBOM: %s", err) } - // Close the file handle before passing the name to the underlying provisioner tmpFileName := tmpFile.Name() if err = tmpFile.Close(); err != nil { return fmt.Errorf("failed to close temporary file for Packer SBOM %s: %s", tmpFileName, err) @@ -282,7 +279,7 @@ func (p *SBOMInternalProvisioner) Provision( defer func(name string) { fileRemoveErr := os.Remove(name) if fileRemoveErr != nil { - log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr) + log.Printf("Error removing SBOM temporary file %s: %s\n", name, fileRemoveErr) } }(tmpFile.Name()) @@ -293,7 +290,7 @@ func (p *SBOMInternalProvisioner) Provision( return err } - format, err := p.validateSBOM(tmpFile.Name()) + sbomFormat, err := p.getSBOMFormat(tmpFile.Name()) if err != nil { return err } @@ -303,7 +300,7 @@ func (p *SBOMInternalProvisioner) Provision( return err } p.CompressedData = compressedData - p.SBOMFormat = format + p.SBOMFormat = sbomFormat return nil } @@ -324,119 +321,17 @@ func (p *SBOMInternalProvisioner) compressFile(filePath string) ([]byte, error) return compressedData, nil } -type SBOMFormat string - -const ( - CycloneDX SBOMFormat = "CycloneDX" - SPDX SBOMFormat = "SPDX" -) - -// SBOMValidator defines the interface for SBOM validation. -type SBOMValidator interface { - Validate(file *os.File) error -} - -// CycloneDxValidator validates CycloneDx SBOM files. -type CycloneDxValidator struct{} - -// Validate performs validation for CycloneDX files. -func (v *CycloneDxValidator) Validate(file *os.File) error { - decoder := cyclonedx.NewBOMDecoder(file, cyclonedx.BOMFileFormatJSON) - bom := new(cyclonedx.BOM) - if err := decoder.Decode(bom); err != nil { - return fmt.Errorf("failed to decode CycloneDX SBOM: %w", err) - } - - if bom.BOMFormat != "CycloneDX" { - return fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat) - } - if bom.SpecVersion.String() == "" { - return fmt.Errorf("specVersion is required") - } - - return nil -} - -// SPDXValidator validates SPDX SBOM files. -type SPDXValidator struct{} - -// Validate performs validation for SPDX files in JSON format. -func (v *SPDXValidator) Validate(file *os.File) error { - doc, err := spdxjson.Read(file) - if err != nil { - return fmt.Errorf("error parsing SPDX JSON file: %w", err) - } - - if doc.SPDXVersion == "" { - return fmt.Errorf("SPDX validation error: missing SPDXVersion") - } - - return nil -} - -// detectSBOMFormat reads the file and detects whether it is a CycloneDX or SPDX file. -func detectSBOMFormat(file *os.File) (SBOMFormat, error) { - // Read a few bytes of the file to determine its type - buffer := make([]byte, 512) - if _, err := file.Read(buffer); err != nil { - return "", fmt.Errorf("failed to read SBOM file: %w", err) - } - - if strings.Contains(string(buffer), "CycloneDX") { - return CycloneDX, nil - } - - if strings.Contains(string(buffer), "SPDX-") { - return SPDX, nil - } - - return "", fmt.Errorf("unsupported or unknown SBOM format") -} - -// NewSBOMValidator is a factory function that returns the appropriate validator based on the file format. -func NewSBOMValidator(format SBOMFormat) (SBOMValidator, error) { - switch format { - case CycloneDX: - return &CycloneDxValidator{}, nil - case SPDX: - return &SPDXValidator{}, nil - default: - return nil, fmt.Errorf("unsupported SBOM format: %s", format) - } -} - -// validateSBOM validates the SBOM file against supported formats (CycloneDx, SPDX). -func (p *SBOMInternalProvisioner) validateSBOM(filePath string) (SBOMFormat, error) { - // Open the SBOM file for reading +func (p *SBOMInternalProvisioner) getSBOMFormat(filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { return "", fmt.Errorf("failed to open SBOM file %s: %w", filePath, err) } defer file.Close() - // Detect the format of the SBOM - format, err := detectSBOMFormat(file) + format, err := hcpSbomProvisioner.ValidateSBOM(file) if err != nil { return "", fmt.Errorf("failed to detect SBOM format: %w", err) } - // Create the appropriate validator - validator, err := NewSBOMValidator(format) - if err != nil { - return "", err - } - - // Seek back to the beginning of the file for validation - if _, err := file.Seek(0, 0); err != nil { - return "", fmt.Errorf("failed to seek SBOM file: %w", err) - } - - // Perform validation using the selected validator - err = validator.Validate(file) - if err != nil { - return "", fmt.Errorf("validation failed for %s format: %w", format, err) - } - - log.Printf(fmt.Sprintf("SBOM file %s is valid for format: %s", filePath, format)) return format, nil } diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index 5b48c58cd..6eae46a80 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -7,6 +7,7 @@ package hcp_sbom import ( + "bytes" "context" "errors" "fmt" @@ -79,19 +80,14 @@ func (p *Provisioner) Provision( ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, ) error { - log.Printf( - fmt.Sprintf("Starting to provision with hcp-sbom using source: %s", - p.config.Source, - ), - ) + log.Println("Starting to provision with `hcp_sbom` provisioner") if generatedData == nil { generatedData = make(map[string]interface{}) } p.config.ctx.Data = generatedData - // Download the files - downloadErr := p.downloadSBOM(ui, comm, generatedData) + downloadErr := p.downloadAndValidateSBOM(ui, comm, generatedData) if downloadErr != nil { return fmt.Errorf("failed to download SBOM file: %w", downloadErr) } @@ -99,44 +95,53 @@ func (p *Provisioner) Provision( return nil } -// downloadSBOM handles downloading SBOM files for the User and Packer. -func (p *Provisioner) downloadSBOM( +// downloadAndValidateSBOM handles downloading SBOM files for the User and Packer. +func (p *Provisioner) downloadAndValidateSBOM( ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, ) error { - // Interpolate the source path src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { return fmt.Errorf("error interpolating source: %s", err) } - // Attempt to download SBOM for User - dst, err := p.getUserDestination() - if err != nil { - return fmt.Errorf("failed to determine user SBOM destination: %s", err) + var buf bytes.Buffer + if err = comm.Download(src, &buf); err != nil { + ui.Error(fmt.Sprintf("download failed for SBOM file: %s", err)) + return err } - // If User SBOM destination is valid, try to download the SBOM file - if dst != "" { - ui.Say(fmt.Sprintf("Attempting to download SBOM file for User: %s", src)) - err = p.downloadToFile(ui, comm, src, dst) - if err != nil { - return fmt.Errorf("user SBOM download failed: %s", err) - } - ui.Say(fmt.Sprintf("User SBOM file successfully downloaded to: %s", dst)) + pkrBuf := bytes.NewBuffer(buf.Bytes()) + usrBuf := bytes.NewBuffer(buf.Bytes()) + if _, err = ValidateSBOM(&buf); err != nil { + ui.Error(fmt.Sprintf("validation failed for SBOM file: %s", err)) + return err } - // Attempt to download SBOM for Packer - dst, err = p.getPackerDestination(generatedData) + // SBOM for Packer + pkrDst, err := p.getPackerDestination(generatedData) if err != nil { return fmt.Errorf("failed to get Packer SBOM destination: %s", err) } - err = p.downloadToFile(ui, comm, src, dst) + err = p.writeToFile(pkrBuf, pkrDst) if err != nil { return fmt.Errorf("failed to download Packer SBOM: %s", err) } + log.Printf("Packer SBOM file successfully downloaded to: %s\n", pkrDst) - ui.Say(fmt.Sprintf("Packer SBOM file successfully downloaded to: %s", dst)) + // SBOM for User + usrDst, err := p.getUserDestination() + if err != nil { + return fmt.Errorf("failed to determine user SBOM destination: %s", err) + } + + if usrDst != "" { + err = p.writeToFile(usrBuf, usrDst) + if err != nil { + return fmt.Errorf("failed to download User SBOM: %s", err) + } + log.Printf("User SBOM file successfully downloaded to: %s\n", usrDst) + } return nil } @@ -193,8 +198,7 @@ func (p *Provisioner) getPackerDestination(generatedData map[string]interface{}) return dst, nil } -// downloadToFile performs the actual download operation to the specified file destination. -func (p *Provisioner) downloadToFile(ui packersdk.Ui, comm packersdk.Communicator, src, dst string) error { +func (p *Provisioner) writeToFile(buf *bytes.Buffer, dst string) error { // Open the destination file f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { @@ -202,10 +206,8 @@ func (p *Provisioner) downloadToFile(ui packersdk.Ui, comm packersdk.Communicato } defer f.Close() - // Download the file - ui.Say(fmt.Sprintf("Downloading SBOM file %s => %s", src, dst)) - if err = comm.Download(src, f); err != nil { - ui.Error(fmt.Sprintf("download failed for SBOM file: %s", err)) + // Write the buffer content to the destination file + if _, err = buf.WriteTo(f); err != nil { return err } diff --git a/provisioner/hcp_sbom/validate.go b/provisioner/hcp_sbom/validate.go new file mode 100644 index 000000000..f6c554a53 --- /dev/null +++ b/provisioner/hcp_sbom/validate.go @@ -0,0 +1,76 @@ +package hcp_sbom + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/CycloneDX/cyclonedx-go" + spdxjson "github.com/spdx/tools-golang/json" +) + +// ValidateCycloneDX is a validation for CycloneDX in JSON format. +func ValidateCycloneDX(content io.Reader) error { + decoder := cyclonedx.NewBOMDecoder(content, cyclonedx.BOMFileFormatJSON) + bom := new(cyclonedx.BOM) + if err := decoder.Decode(bom); err != nil { + return fmt.Errorf("error parsing CycloneDX SBOM: %w", err) + } + + if bom.BOMFormat != "CycloneDX" { + return fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat) + } + if bom.SpecVersion.String() == "" { + return fmt.Errorf("specVersion is required") + } + + return nil +} + +// ValidateSPDX is a validation for SPDX in JSON format. +func ValidateSPDX(content io.Reader) error { + doc, err := spdxjson.Read(content) + if err != nil { + return fmt.Errorf("error parsing SPDX JSON file: %w", err) + } + + if doc.SPDXVersion == "" { + return fmt.Errorf("SPDX validation error: missing SPDXVersion") + } + + return nil +} + +// ValidateSBOM validates the SBOM file and returns the format of the SBOM. +func ValidateSBOM(content io.Reader) (string, error) { + var buf bytes.Buffer + if _, err := io.Copy(&buf, content); err != nil { + return "", fmt.Errorf("failed to copy content: %s", err) + } + + reader := bytes.NewReader(buf.Bytes()) + + spdxErr := ValidateSPDX(reader) + if spdxErr == nil { + return "spdx", nil + } + if !strings.Contains(spdxErr.Error(), "error parsing") { + return "", spdxErr + } + + // Reset the reader's position + if _, err := reader.Seek(0, io.SeekStart); err != nil { + return "", fmt.Errorf("failed to reset reader: %s", err) + } + + cycloneDxErr := ValidateCycloneDX(reader) + if cycloneDxErr == nil { + return "cyclonedx", nil + } + if !strings.Contains(cycloneDxErr.Error(), "error parsing") { + return "", cycloneDxErr + } + + return "", fmt.Errorf("error validating SBOM file: invalid SBOM format") +} From c39d3906fb18b71c6e5ac5bc63ff40e19bbda4d9 Mon Sep 17 00:00:00 2001 From: Devashish Date: Tue, 15 Oct 2024 14:56:43 -0400 Subject: [PATCH 08/11] Rename hcp_sbom to hcp-sbom --- command/execute.go | 4 ++-- hcl2template/types.packer_config.go | 2 +- packer/core.go | 2 +- packer/provisioner.go | 6 +++--- provisioner/{hcp_sbom => hcp-sbom}/provisioner.go | 2 +- provisioner/{hcp_sbom => hcp-sbom}/provisioner.hcl2spec.go | 0 provisioner/{hcp_sbom => hcp-sbom}/validate.go | 0 provisioner/{hcp_sbom => hcp-sbom}/version/version.go | 0 .../{hcp_sbom => hcp-sbom}/Config-not-required.mdx | 4 ++-- .../provisioner/{hcp_sbom => hcp-sbom}/Config-required.mdx | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) rename provisioner/{hcp_sbom => hcp-sbom}/provisioner.go (99%) rename provisioner/{hcp_sbom => hcp-sbom}/provisioner.hcl2spec.go (100%) rename provisioner/{hcp_sbom => hcp-sbom}/validate.go (100%) rename provisioner/{hcp_sbom => hcp-sbom}/version/version.go (100%) rename website/content/partials/provisioner/{hcp_sbom => hcp-sbom}/Config-not-required.mdx (88%) rename website/content/partials/provisioner/{hcp_sbom => hcp-sbom}/Config-required.mdx (72%) diff --git a/command/execute.go b/command/execute.go index 308c159c3..1e303858d 100644 --- a/command/execute.go +++ b/command/execute.go @@ -28,7 +28,7 @@ import ( shelllocalpostprocessor "github.com/hashicorp/packer/post-processor/shell-local" breakpointprovisioner "github.com/hashicorp/packer/provisioner/breakpoint" fileprovisioner "github.com/hashicorp/packer/provisioner/file" - hcp_sbomprovisioner "github.com/hashicorp/packer/provisioner/hcp_sbom" + hcpsbomprovisioner "github.com/hashicorp/packer/provisioner/hcp-sbom" powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell" shellprovisioner "github.com/hashicorp/packer/provisioner/shell" shelllocalprovisioner "github.com/hashicorp/packer/provisioner/shell-local" @@ -49,7 +49,7 @@ var Builders = map[string]packersdk.Builder{ var Provisioners = map[string]packersdk.Provisioner{ "breakpoint": new(breakpointprovisioner.Provisioner), "file": new(fileprovisioner.Provisioner), - "hcp_sbom": new(hcp_sbomprovisioner.Provisioner), + "hcp-sbom": new(hcpsbomprovisioner.Provisioner), "powershell": new(powershellprovisioner.Provisioner), "shell": new(shellprovisioner.Provisioner), "shell-local": new(shelllocalprovisioner.Provisioner), diff --git a/hcl2template/types.packer_config.go b/hcl2template/types.packer_config.go index 49ca88729..1f909aca3 100644 --- a/hcl2template/types.packer_config.go +++ b/hcl2template/types.packer_config.go @@ -516,7 +516,7 @@ func (cfg *PackerConfig) getCoreBuildProvisioner(source SourceUseBlock, pb *Prov } } - if pb.PType == "hcp_sbom" { + if pb.PType == "hcp-sbom" { provisioner = &packer.SBOMInternalProvisioner{ Provisioner: provisioner, } diff --git a/packer/core.go b/packer/core.go index 66e41a7e8..9d574294c 100644 --- a/packer/core.go +++ b/packer/core.go @@ -297,7 +297,7 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName } } - if rawP.Type == "hcp_sbom" { + if rawP.Type == "hcp-sbom" { provisioner = &SBOMInternalProvisioner{ Provisioner: provisioner, } diff --git a/packer/provisioner.go b/packer/provisioner.go index fcea43a64..1bdee785d 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -9,7 +9,7 @@ import ( "log" "os" - hcpSbomProvisioner "github.com/hashicorp/packer/provisioner/hcp_sbom" + hcpSbomProvisioner "github.com/hashicorp/packer/provisioner/hcp-sbom" "github.com/klauspost/compress/zstd" @@ -241,9 +241,9 @@ func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, co return p.Provisioner.Provision(ctx, ui, comm, generatedData) } -// SBOMInternalProvisioner is a wrapper provisioner for the `hcp_sbom` provisioner +// SBOMInternalProvisioner is a wrapper provisioner for the `hcp-sbom` provisioner // that sets the path for SBOM file download and, after the successful execution of -// the `hcp_sbom` provisioner, compresses the SBOM and prepares the data for API +// the `hcp-sbom` provisioner, compresses the SBOM and prepares the data for API // integration. type SBOMInternalProvisioner struct { Provisioner packersdk.Provisioner diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp-sbom/provisioner.go similarity index 99% rename from provisioner/hcp_sbom/provisioner.go rename to provisioner/hcp-sbom/provisioner.go index 6eae46a80..1f6555862 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp-sbom/provisioner.go @@ -80,7 +80,7 @@ func (p *Provisioner) Provision( ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, ) error { - log.Println("Starting to provision with `hcp_sbom` provisioner") + log.Println("Starting to provision with `hcp-sbom` provisioner") if generatedData == nil { generatedData = make(map[string]interface{}) diff --git a/provisioner/hcp_sbom/provisioner.hcl2spec.go b/provisioner/hcp-sbom/provisioner.hcl2spec.go similarity index 100% rename from provisioner/hcp_sbom/provisioner.hcl2spec.go rename to provisioner/hcp-sbom/provisioner.hcl2spec.go diff --git a/provisioner/hcp_sbom/validate.go b/provisioner/hcp-sbom/validate.go similarity index 100% rename from provisioner/hcp_sbom/validate.go rename to provisioner/hcp-sbom/validate.go diff --git a/provisioner/hcp_sbom/version/version.go b/provisioner/hcp-sbom/version/version.go similarity index 100% rename from provisioner/hcp_sbom/version/version.go rename to provisioner/hcp-sbom/version/version.go diff --git a/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx b/website/content/partials/provisioner/hcp-sbom/Config-not-required.mdx similarity index 88% rename from website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx rename to website/content/partials/provisioner/hcp-sbom/Config-not-required.mdx index a46cec04b..eb241a06c 100644 --- a/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx +++ b/website/content/partials/provisioner/hcp-sbom/Config-not-required.mdx @@ -1,4 +1,4 @@ - + - `destination` (string) - Destination is an optional field that specifies the path where the SBOM file will be downloaded to for the user. @@ -10,4 +10,4 @@ a "Permission Denied" error will occur. If the source path is a file, it is recommended that the destination path be a file as well. - + diff --git a/website/content/partials/provisioner/hcp_sbom/Config-required.mdx b/website/content/partials/provisioner/hcp-sbom/Config-required.mdx similarity index 72% rename from website/content/partials/provisioner/hcp_sbom/Config-required.mdx rename to website/content/partials/provisioner/hcp-sbom/Config-required.mdx index 936c435f6..2f227c2b0 100644 --- a/website/content/partials/provisioner/hcp_sbom/Config-required.mdx +++ b/website/content/partials/provisioner/hcp-sbom/Config-required.mdx @@ -1,7 +1,7 @@ - + - `source` (string) - Source is a required field that specifies the path to the SBOM file that needs to be downloaded. It can be a file path or a URL. - + From b2329ca1a95036e277c32fd69444735099f7f4a8 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 21 Oct 2024 16:30:48 -0400 Subject: [PATCH 09/11] Typed error check in validation --- packer/provisioner.go | 2 +- provisioner/hcp-sbom/validate.go | 60 +++++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/packer/provisioner.go b/packer/provisioner.go index 1bdee785d..24d670950 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -279,7 +279,7 @@ func (p *SBOMInternalProvisioner) Provision( defer func(name string) { fileRemoveErr := os.Remove(name) if fileRemoveErr != nil { - log.Printf("Error removing SBOM temporary file %s: %s\n", name, fileRemoveErr) + log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr) } }(tmpFile.Name()) diff --git a/provisioner/hcp-sbom/validate.go b/provisioner/hcp-sbom/validate.go index f6c554a53..35b6299bb 100644 --- a/provisioner/hcp-sbom/validate.go +++ b/provisioner/hcp-sbom/validate.go @@ -3,26 +3,55 @@ package hcp_sbom import ( "bytes" "fmt" - "io" - "strings" - "github.com/CycloneDX/cyclonedx-go" spdxjson "github.com/spdx/tools-golang/json" + "io" ) +// ErrorType represents the type of validation error. +type ErrorType string + +const ( + ParsingErr ErrorType = "parsing" + ValidationErr ErrorType = "validation" +) + +// ValidationError represents an error encountered while validating an SBOM. +type ValidationError struct { + Type ErrorType + Err error +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf(" %s error: %v", e.Type, e.Err) +} + +func (e *ValidationError) Unwrap() error { + return e.Err +} + // ValidateCycloneDX is a validation for CycloneDX in JSON format. func ValidateCycloneDX(content io.Reader) error { decoder := cyclonedx.NewBOMDecoder(content, cyclonedx.BOMFileFormatJSON) bom := new(cyclonedx.BOM) if err := decoder.Decode(bom); err != nil { - return fmt.Errorf("error parsing CycloneDX SBOM: %w", err) + return &ValidationError{ + Type: ParsingErr, + Err: fmt.Errorf("error parsing CycloneDX SBOM: %w", err), + } } if bom.BOMFormat != "CycloneDX" { - return fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat) + return &ValidationError{ + Type: ValidationErr, + Err: fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat), + } } if bom.SpecVersion.String() == "" { - return fmt.Errorf("specVersion is required") + return &ValidationError{ + Type: ValidationErr, + Err: fmt.Errorf("specVersion is required"), + } } return nil @@ -32,11 +61,17 @@ func ValidateCycloneDX(content io.Reader) error { func ValidateSPDX(content io.Reader) error { doc, err := spdxjson.Read(content) if err != nil { - return fmt.Errorf("error parsing SPDX JSON file: %w", err) + return &ValidationError{ + Type: ParsingErr, + Err: fmt.Errorf("error parsing SPDX JSON file: %w", err), + } } if doc.SPDXVersion == "" { - return fmt.Errorf("SPDX validation error: missing SPDXVersion") + return &ValidationError{ + Type: ValidationErr, + Err: fmt.Errorf("missing SPDXVersion"), + } } return nil @@ -51,11 +86,11 @@ func ValidateSBOM(content io.Reader) (string, error) { reader := bytes.NewReader(buf.Bytes()) + // Try validating as SPDX spdxErr := ValidateSPDX(reader) if spdxErr == nil { return "spdx", nil - } - if !strings.Contains(spdxErr.Error(), "error parsing") { + } else if vErr, ok := spdxErr.(*ValidationError); ok && vErr.Type == ValidationErr { return "", spdxErr } @@ -67,9 +102,8 @@ func ValidateSBOM(content io.Reader) (string, error) { cycloneDxErr := ValidateCycloneDX(reader) if cycloneDxErr == nil { return "cyclonedx", nil - } - if !strings.Contains(cycloneDxErr.Error(), "error parsing") { - return "", cycloneDxErr + } else if vErr, ok := cycloneDxErr.(*ValidationError); ok && vErr.Type == ValidationErr { + return "", spdxErr } return "", fmt.Errorf("error validating SBOM file: invalid SBOM format") From 8807cea7f3a89809fb8ff174c87134b0d9d020d3 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 21 Oct 2024 16:54:02 -0400 Subject: [PATCH 10/11] Use single buffer --- provisioner/hcp-sbom/provisioner.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/provisioner/hcp-sbom/provisioner.go b/provisioner/hcp-sbom/provisioner.go index 1f6555862..cddb9b1f2 100644 --- a/provisioner/hcp-sbom/provisioner.go +++ b/provisioner/hcp-sbom/provisioner.go @@ -11,6 +11,7 @@ import ( "context" "errors" "fmt" + "io" "log" "os" @@ -101,21 +102,21 @@ func (p *Provisioner) downloadAndValidateSBOM( ) error { src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { - return fmt.Errorf("error interpolating source: %s", err) + return fmt.Errorf("error interpolating SBOM source: %s", err) } var buf bytes.Buffer if err = comm.Download(src, &buf); err != nil { - ui.Error(fmt.Sprintf("download failed for SBOM file: %s", err)) + ui.Errorf("download failed for SBOM file: %s", err) return err } - pkrBuf := bytes.NewBuffer(buf.Bytes()) - usrBuf := bytes.NewBuffer(buf.Bytes()) - if _, err = ValidateSBOM(&buf); err != nil { - ui.Error(fmt.Sprintf("validation failed for SBOM file: %s", err)) + reader := bytes.NewReader(buf.Bytes()) + if _, err = ValidateSBOM(reader); err != nil { + ui.Errorf("validation failed for SBOM file: %s", err) return err } + reader.Seek(0, io.SeekStart) // SBOM for Packer pkrDst, err := p.getPackerDestination(generatedData) @@ -123,10 +124,11 @@ func (p *Provisioner) downloadAndValidateSBOM( return fmt.Errorf("failed to get Packer SBOM destination: %s", err) } - err = p.writeToFile(pkrBuf, pkrDst) + err = p.writeToFile(reader, pkrDst) if err != nil { return fmt.Errorf("failed to download Packer SBOM: %s", err) } + reader.Seek(0, io.SeekStart) log.Printf("Packer SBOM file successfully downloaded to: %s\n", pkrDst) // SBOM for User @@ -136,7 +138,7 @@ func (p *Provisioner) downloadAndValidateSBOM( } if usrDst != "" { - err = p.writeToFile(usrBuf, usrDst) + err = p.writeToFile(reader, usrDst) if err != nil { return fmt.Errorf("failed to download User SBOM: %s", err) } @@ -198,7 +200,7 @@ func (p *Provisioner) getPackerDestination(generatedData map[string]interface{}) return dst, nil } -func (p *Provisioner) writeToFile(buf *bytes.Buffer, dst string) error { +func (p *Provisioner) writeToFile(buf *bytes.Reader, dst string) error { // Open the destination file f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { From 9df2e934dbcafe97d313c05415dc487edef95f9d Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 21 Oct 2024 17:03:44 -0400 Subject: [PATCH 11/11] Lint --- provisioner/hcp-sbom/provisioner.go | 10 ++++++++-- provisioner/hcp-sbom/validate.go | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/provisioner/hcp-sbom/provisioner.go b/provisioner/hcp-sbom/provisioner.go index cddb9b1f2..2815c6657 100644 --- a/provisioner/hcp-sbom/provisioner.go +++ b/provisioner/hcp-sbom/provisioner.go @@ -116,7 +116,10 @@ func (p *Provisioner) downloadAndValidateSBOM( ui.Errorf("validation failed for SBOM file: %s", err) return err } - reader.Seek(0, io.SeekStart) + _, err = reader.Seek(0, io.SeekStart) + if err != nil { + return err + } // SBOM for Packer pkrDst, err := p.getPackerDestination(generatedData) @@ -128,7 +131,10 @@ func (p *Provisioner) downloadAndValidateSBOM( if err != nil { return fmt.Errorf("failed to download Packer SBOM: %s", err) } - reader.Seek(0, io.SeekStart) + _, err = reader.Seek(0, io.SeekStart) + if err != nil { + return err + } log.Printf("Packer SBOM file successfully downloaded to: %s\n", pkrDst) // SBOM for User diff --git a/provisioner/hcp-sbom/validate.go b/provisioner/hcp-sbom/validate.go index 35b6299bb..372d61507 100644 --- a/provisioner/hcp-sbom/validate.go +++ b/provisioner/hcp-sbom/validate.go @@ -3,8 +3,10 @@ package hcp_sbom import ( "bytes" "fmt" + "github.com/CycloneDX/cyclonedx-go" spdxjson "github.com/spdx/tools-golang/json" + "io" )