From 10a107cce5d3dde922fa9fe3b71c1e89892b47e9 Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 18 Sep 2024 14:25:16 -0400 Subject: [PATCH 01/27] Add outer provisioner to download, validate and compress SBOM --- command/execute.go | 2 + provisioner/hcp_sbom/provisioner.go | 239 ++++++++++++++++++ provisioner/hcp_sbom/provisioner.hcl2spec.go | 49 ++++ provisioner/hcp_sbom/provisioner_test.go | 219 ++++++++++++++++ provisioner/hcp_sbom/test-sbom.json | 1 + provisioner/hcp_sbom/version/version.go | 16 ++ .../hcp_sbom/Config-not-required.mdx | 5 + .../provisioner/hcp_sbom/Config-required.mdx | 5 + 8 files changed, 536 insertions(+) create mode 100644 provisioner/hcp_sbom/provisioner.go create mode 100644 provisioner/hcp_sbom/provisioner.hcl2spec.go create mode 100644 provisioner/hcp_sbom/provisioner_test.go create mode 100644 provisioner/hcp_sbom/test-sbom.json create mode 100644 provisioner/hcp_sbom/version/version.go create mode 100644 website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx create mode 100644 website/content/partials/provisioner/hcp_sbom/Config-required.mdx diff --git a/command/execute.go b/command/execute.go index 7ad74f314d4..308c159c333 100644 --- a/command/execute.go +++ b/command/execute.go @@ -28,6 +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" powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell" shellprovisioner "github.com/hashicorp/packer/provisioner/shell" shelllocalprovisioner "github.com/hashicorp/packer/provisioner/shell-local" @@ -48,6 +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), "powershell": new(powershellprovisioner.Provisioner), "shell": new(shellprovisioner.Provisioner), "shell-local": new(shelllocalprovisioner.Provisioner), diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go new file mode 100644 index 00000000000..6924835402e --- /dev/null +++ b/provisioner/hcp_sbom/provisioner.go @@ -0,0 +1,239 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:generate packer-sdc mapstructure-to-hcl2 -type Config +//go:generate packer-sdc struct-markdown + +package hcp_sbom + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "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" + "github.com/klauspost/compress/zstd" + "io" + "os" + "path/filepath" + "strings" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + Source string `mapstructure:"source" required:"true"` + Destination string `mapstructure:"destination"` + ctx interpolate.Context +} + +type Provisioner struct { + config Config +} + +func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { + return p.config.FlatMapstructure().HCL2Spec() +} + +func (p *Provisioner) Prepare(raws ...interface{}) error { + err := config.Decode(&p.config, &config.DecodeOpts{ + PluginType: "hcp-sbom", + Interpolate: true, + InterpolateContext: &p.config.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{}, + }, + }, raws...) + if err != nil { + return err + } + + var errs *packersdk.MultiError + if p.config.Source == "" { + errs = packersdk.MultiErrorAppend(errs, errors.New("source must be specified")) + } + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + + return nil +} + +func (p *Provisioner) Provision( + ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, +) error { + ui.Say( + fmt.Sprintf("Starting to provision with hcp-sbom using source: %s", + p.config.Source, + ), + ) + + if generatedData == nil { + generatedData = make(map[string]interface{}) + } + p.config.ctx.Data = generatedData + + // Download the file + destPath, downloadErr := p.downloadSBOM(ui, comm) + // defer os.Remove(destPath) + if downloadErr != nil { + return fmt.Errorf("failed to download file: %w", downloadErr) + } + + // Validate the file + ui.Say(fmt.Sprintf("Validating SBOM file %s", destPath)) + validationErr := p.validateSBOM(ui, destPath) + if validationErr != nil { + return fmt.Errorf("failed to validate SBOM file: %w", validationErr) + } + + // Compress the file + ui.Say(fmt.Sprintf("Compressing SBOM file %s", destPath)) + _, compessionErr := p.compressFile(ui, destPath) + if compessionErr != nil { + return fmt.Errorf("failed to compress file: %w", compessionErr) + } + + // Future: send compressedData to the internal API as per RFC + // ... + + return nil +} + +// downloadSBOM downloads a Software Bill of Materials (SBOM) from a specified +// source to a local destination. It works with all communicators from packersdk. +// The method returns the path to the downloaded file or an error if any issues +// occur during the download process. +func (p *Provisioner) downloadSBOM(ui packersdk.Ui, comm packersdk.Communicator) (string, error) { + src, err := interpolate.Render(p.config.Source, &p.config.ctx) + if err != nil { + return p.config.Destination, fmt.Errorf("error interpolating source: %s", err) + } + + // 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, + ) + } + + // Determine the destination path + dst := p.config.Destination + if dst == "" { + tmpFile, err := os.CreateTemp("", "packer-sbom-*.json") + if err != nil { + return dst, fmt.Errorf( + "failed to create file for Packer SBOM: %s", err, + ) + } + dst = tmpFile.Name() + tmpFile.Close() + } else { + dst, err = interpolate.Render(dst, &p.config.ctx) + if err != nil { + return dst, fmt.Errorf("error interpolating Packer SBOM destination: %s", err) + } + + if strings.HasSuffix(dst, "/") { + info, err := os.Stat(dst) + if err != nil { + return dst, fmt.Errorf("failed to stat destination for Packer SBOM: %s", err) + } + + if info.IsDir() { + tmpFile, err := os.CreateTemp(dst, "packer-sbom-*.json") + if err != nil { + return dst, fmt.Errorf("failed to create file for Packer SBOM: %s", err) + } + dst = tmpFile.Name() + tmpFile.Close() + } + } + } + + // 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) + } + + // 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) + } + defer f.Close() + + // Create MultiWriter for the current progress + pf := io.MultiWriter(f) + + // Download the file + ui.Say(fmt.Sprintf("Downloading SBOM file %s => %s", src, dst)) + if err = comm.Download(src, pf); err != nil { + ui.Error(fmt.Sprintf("download failed for SBOM file: %s", err)) + return dst, err + } + + return dst, nil +} + +func (p *Provisioner) compressFile(ui packersdk.Ui, filePath string) ([]byte, error) { + sourceFile, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer sourceFile.Close() + + data, err := io.ReadAll(sourceFile) + if err != nil { + return nil, err + } + + encoder, err := zstd.NewWriter(nil) + if err != nil { + return nil, err + } + defer encoder.Close() + + compressedData := encoder.EncodeAll(data, nil) + + ui.Say(fmt.Sprintf("SBOM file compressed successfully. Size: %d bytes", len(compressedData))) + return compressedData, nil +} + +type SBOM struct { + BomFormat string `json:"bomFormat"` + SpecVersion string `json:"specVersion"` +} + +func (p *Provisioner) validateSBOM(ui packersdk.Ui, filePath string) error { + sourceFile, err := os.Open(filePath) + if err != nil { + return err + } + defer sourceFile.Close() + + data, err := io.ReadAll(sourceFile) + if err != nil { + return err + } + + var sbom SBOM + if err := json.Unmarshal(data, &sbom); err != nil { + return fmt.Errorf("failed to unmarshal JSON: %w", err) + } + + if sbom.BomFormat != "CycloneDX" { + return fmt.Errorf("invalid bomFormat: %s", sbom.BomFormat) + } + + if sbom.SpecVersion == "" { + return fmt.Errorf("specVersion is required") + } + + return nil +} diff --git a/provisioner/hcp_sbom/provisioner.hcl2spec.go b/provisioner/hcp_sbom/provisioner.hcl2spec.go new file mode 100644 index 00000000000..e6ee5d22753 --- /dev/null +++ b/provisioner/hcp_sbom/provisioner.hcl2spec.go @@ -0,0 +1,49 @@ +// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. + +package hcp_sbom + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// FlatConfig is an auto-generated flat version of Config. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatConfig struct { + PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` + PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` + PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"` + PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"` + PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"` + PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"` + PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` + PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` + Source *string `mapstructure:"source" required:"true" cty:"source" hcl:"source"` + Destination *string `mapstructure:"destination" cty:"destination" hcl:"destination"` +} + +// FlatMapstructure returns a new FlatConfig. +// FlatConfig is an auto-generated flat version of Config. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatConfig) +} + +// HCL2Spec returns the hcl spec of a Config. +// This spec is used by HCL to read the fields of Config. +// The decoded values from this spec will then be applied to a FlatConfig. +func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, + "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, + "packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false}, + "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, + "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, + "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, + "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, + "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, + "source": &hcldec.AttrSpec{Name: "source", Type: cty.String, Required: false}, + "destination": &hcldec.AttrSpec{Name: "destination", Type: cty.String, Required: false}, + } + return s +} diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go new file mode 100644 index 00000000000..b9adb58213f --- /dev/null +++ b/provisioner/hcp_sbom/provisioner_test.go @@ -0,0 +1,219 @@ +package hcp_sbom + +import ( + "encoding/json" + "fmt" + "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/klauspost/compress/zstd" + "io" + "os" + "testing" +) + +type MockUi struct { + packer.Ui +} + +func (m *MockUi) Say(message string) { + fmt.Println(message) +} + +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 TestDownloadSBOM(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: true, + }, + { + 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: true, + }, + { + 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, + } + + destPath, err := provisioner.downloadSBOM(ui, comm) + 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{} + + tests := []struct { + name string + sbom SBOM + expectError bool + errorMsg string + }{ + { + name: "Valid SBOM", + sbom: SBOM{ + BomFormat: "CycloneDX", + SpecVersion: "1.0", + }, + expectError: false, + }, + { + name: "Invalid BomFormat", + sbom: SBOM{ + BomFormat: "InvalidFormat", + SpecVersion: "1.0", + }, + expectError: true, + errorMsg: "invalid bomFormat: InvalidFormat", + }, + { + name: "Empty SpecVersion", + sbom: SBOM{ + BomFormat: "CycloneDX", + SpecVersion: "", + }, + expectError: true, + errorMsg: "specVersion is required", + }, + } + + 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) + } + } + }) + } +} + +func TestCompressFile(t *testing.T) { + ui := &MockUi{} + provisioner := &Provisioner{} + validSBOM := SBOM{ + BomFormat: "CycloneDX", + SpecVersion: "1.0", + } + data, _ := json.Marshal(validSBOM) + filePath := "data.json" + //os.WriteFile(filePath, data, 0644) + //defer os.Remove(filePath) + + sourceFile, err := os.Open(filePath) + if err != nil { + t.Fatalf("expected no error:%v", err) + } + defer sourceFile.Close() + + data, err = io.ReadAll(sourceFile) + if err != nil { + t.Fatalf("expected no error:%v", err) + } + + compressedData, err := provisioner.compressFile(ui, filePath) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + decoder, err := zstd.NewReader(nil) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + defer decoder.Close() + + decompressedData, err := decoder.DecodeAll(compressedData, nil) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if string(decompressedData) != string(data) { + t.Fatalf("expected decompressed data to be '%s', got %s", data, decompressedData) + } +} diff --git a/provisioner/hcp_sbom/test-sbom.json b/provisioner/hcp_sbom/test-sbom.json new file mode 100644 index 00000000000..fc1badee633 --- /dev/null +++ b/provisioner/hcp_sbom/test-sbom.json @@ -0,0 +1 @@ +{"bomFormat":"InvalidFormat","specVersion":"1.0"} \ No newline at end of file diff --git a/provisioner/hcp_sbom/version/version.go b/provisioner/hcp_sbom/version/version.go new file mode 100644 index 00000000000..772d6d4f444 --- /dev/null +++ b/provisioner/hcp_sbom/version/version.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package version + +import ( + "github.com/hashicorp/packer-plugin-sdk/version" + packerVersion "github.com/hashicorp/packer/version" +) + +var HCPSBOMPluginVersion *version.PluginVersion + +func init() { + HCPSBOMPluginVersion = version.NewPluginVersion( + packerVersion.Version, packerVersion.VersionPrerelease, packerVersion.VersionMetadata) +} diff --git a/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx b/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx new file mode 100644 index 00000000000..a8019fbdef1 --- /dev/null +++ b/website/content/partials/provisioner/hcp_sbom/Config-not-required.mdx @@ -0,0 +1,5 @@ + + +- `destination` (string) - Destination + + diff --git a/website/content/partials/provisioner/hcp_sbom/Config-required.mdx b/website/content/partials/provisioner/hcp_sbom/Config-required.mdx new file mode 100644 index 00000000000..0cb7e7a8092 --- /dev/null +++ b/website/content/partials/provisioner/hcp_sbom/Config-required.mdx @@ -0,0 +1,5 @@ + + +- `source` (string) - Source + + From e3598982c84806c086f6a84cd4c9b9959cde4240 Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 25 Sep 2024 14:04:03 -0400 Subject: [PATCH 02/27] Add and set up internal SBOM provisioner --- hcl2template/types.packer_config.go | 6 +++ packer/build.go | 28 +++++++++++ packer/core.go | 7 +++ packer/provisioner.go | 75 +++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/hcl2template/types.packer_config.go b/hcl2template/types.packer_config.go index 51608b7413d..49ca8872912 100644 --- a/hcl2template/types.packer_config.go +++ b/hcl2template/types.packer_config.go @@ -516,6 +516,12 @@ func (cfg *PackerConfig) getCoreBuildProvisioner(source SourceUseBlock, pb *Prov } } + if pb.PType == "hcp_sbom" { + provisioner = &packer.SBOMInternalProvisioner{ + Provisioner: provisioner, + } + } + return packer.CoreBuildProvisioner{ PType: pb.PType, PName: pb.PName, diff --git a/packer/build.go b/packer/build.go index 8b62ec53799..65b084565de 100644 --- a/packer/build.go +++ b/packer/build.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "log" + "path/filepath" "sync" "github.com/hashicorp/packer-plugin-sdk/common" @@ -50,11 +51,20 @@ type CoreBuild struct { onError string l sync.Mutex prepareCalled bool + + SBOMFileName string + SBOMFileCompressed []byte +} + +type SBOM struct { + FileName string + CompressedData []byte } type BuildMetadata struct { PackerVersion string Plugins map[string]PluginDetails + SBOM SBOM } func (b *CoreBuild) getPluginsMetadata() map[string]PluginDetails { @@ -88,6 +98,10 @@ func (b *CoreBuild) GetMetadata() BuildMetadata { metadata := BuildMetadata{ PackerVersion: version.FormattedVersion(), Plugins: b.getPluginsMetadata(), + SBOM: SBOM{ + FileName: b.SBOMFileName, + CompressedData: b.SBOMFileCompressed, + }, } return metadata } @@ -300,6 +314,20 @@ 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 + } + b.SBOMFileName = filepath.Base(sbomInternalProvisioner.TempFileLoc) + b.SBOMFileCompressed = sbomInternalProvisioner.CompressedData + + fmt.Printf("==== SBOM File Name: %v ====\n", b.SBOMFileName) + fmt.Printf("==== SBOM Compressed Data: %v ====\n", len(sbomInternalProvisioner.CompressedData)) + } + } + // If there was no result, don't worry about running post-processors // because there is nothing they can do, just return. if builderArtifact == nil { diff --git a/packer/core.go b/packer/core.go index f6e3926c4a7..66e41a7e84d 100644 --- a/packer/core.go +++ b/packer/core.go @@ -296,6 +296,13 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName Provisioner: provisioner, } } + + if rawP.Type == "hcp_sbom" { + provisioner = &SBOMInternalProvisioner{ + Provisioner: provisioner, + } + } + cbp = CoreBuildProvisioner{ PType: rawP.Type, Provisioner: provisioner, diff --git a/packer/provisioner.go b/packer/provisioner.go index 81dce0ecfc0..e2f7ccf791c 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -6,7 +6,10 @@ package packer import ( "context" "fmt" + "github.com/klauspost/compress/zstd" + "io" "log" + "os" "time" "github.com/hashicorp/hcl/v2/hcldec" @@ -234,3 +237,75 @@ 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. +type SBOMInternalProvisioner struct { + Provisioner packersdk.Provisioner + TempFileLoc string + CompressedData []byte +} + +func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() } +func (p *SBOMInternalProvisioner) FlatConfig() interface{} { return p.FlatConfig() } +func (p *SBOMInternalProvisioner) Prepare(raws ...interface{}) error { + return p.Provisioner.Prepare(raws...) +} + +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) + } + generatedData["dst"] = tmpFile.Name() + p.TempFileLoc = tmpFile.Name() + ctx = context.WithValue(ctx, "sbomFilePath", tmpFile.Name()) + tmpFile.Close() + + err = p.Provisioner.Provision(ctx, ui, comm, generatedData) + if err != nil { + return err + } + + compressedData, err := p.compressFile(p.TempFileLoc) + if err != nil { + return err + } + p.CompressedData = compressedData + os.Remove(p.TempFileLoc) + return nil +} + +func (p *SBOMInternalProvisioner) compressFile(filePath string) ([]byte, error) { + sourceFile, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer sourceFile.Close() + + data, err := io.ReadAll(sourceFile) + if err != nil { + return nil, err + } + + encoder, err := zstd.NewWriter(nil) + if err != nil { + return nil, err + } + defer encoder.Close() + + compressedData := encoder.EncodeAll(data, nil) + + fmt.Printf(fmt.Sprintf("SBOM file compressed successfully. Size: %d bytes", len(compressedData))) + return compressedData, nil +} From 8ab261e25aa5ec41549d745f0403c0c41a96c258 Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 25 Sep 2024 14:04:24 -0400 Subject: [PATCH 03/27] Modify external provisioner --- provisioner/hcp_sbom/provisioner.go | 143 ++++++++++++----------- provisioner/hcp_sbom/provisioner_test.go | 49 +------- 2 files changed, 79 insertions(+), 113 deletions(-) diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index 6924835402e..eff5647226d 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -16,7 +16,6 @@ import ( packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" - "github.com/klauspost/compress/zstd" "io" "os" "path/filepath" @@ -64,7 +63,8 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } func (p *Provisioner) Provision( - ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, + ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, + generatedData map[string]interface{}, ) error { ui.Say( fmt.Sprintf("Starting to provision with hcp-sbom using source: %s", @@ -77,13 +77,15 @@ func (p *Provisioner) Provision( } p.config.ctx.Data = generatedData - // Download the file - destPath, downloadErr := p.downloadSBOM(ui, comm) - // defer os.Remove(destPath) + // Download the file for Packer + destPath, downloadErr := p.downloadSBOMForPacker(ui, comm, generatedData) if downloadErr != nil { return fmt.Errorf("failed to download file: %w", downloadErr) } + // Download the file for user + p.downloadSBOMForUser(ui, comm) + // Validate the file ui.Say(fmt.Sprintf("Validating SBOM file %s", destPath)) validationErr := p.validateSBOM(ui, destPath) @@ -91,29 +93,21 @@ func (p *Provisioner) Provision( return fmt.Errorf("failed to validate SBOM file: %w", validationErr) } - // Compress the file - ui.Say(fmt.Sprintf("Compressing SBOM file %s", destPath)) - _, compessionErr := p.compressFile(ui, destPath) - if compessionErr != nil { - return fmt.Errorf("failed to compress file: %w", compessionErr) - } - - // Future: send compressedData to the internal API as per RFC - // ... - return nil } -// downloadSBOM downloads a Software Bill of Materials (SBOM) from a specified -// source to a local destination. It works with all communicators from packersdk. -// The method returns the path to the downloaded file or an error if any issues -// occur during the download process. -func (p *Provisioner) downloadSBOM(ui packersdk.Ui, comm packersdk.Communicator) (string, error) { +// 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( + ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}, +) (string, error) { src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { 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( @@ -121,40 +115,13 @@ func (p *Provisioner) downloadSBOM(ui packersdk.Ui, comm packersdk.Communicator) ) } - // Determine the destination path - dst := p.config.Destination - if dst == "" { - tmpFile, err := os.CreateTemp("", "packer-sbom-*.json") - if err != nil { - return dst, fmt.Errorf( - "failed to create file for Packer SBOM: %s", err, - ) - } - dst = tmpFile.Name() - tmpFile.Close() - } else { - dst, err = interpolate.Render(dst, &p.config.ctx) - if err != nil { - return dst, fmt.Errorf("error interpolating Packer SBOM destination: %s", err) - } - - if strings.HasSuffix(dst, "/") { - info, err := os.Stat(dst) - if err != nil { - return dst, fmt.Errorf("failed to stat destination for Packer SBOM: %s", err) - } - - if info.IsDir() { - tmpFile, err := os.CreateTemp(dst, "packer-sbom-*.json") - if err != nil { - return dst, fmt.Errorf("failed to create file for Packer SBOM: %s", err) - } - dst = tmpFile.Name() - tmpFile.Close() - } - } + // 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 := fmt.Sprintf("%v", desti) // Ensure the destination directory exists dir := filepath.Dir(dst) if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil { @@ -172,37 +139,81 @@ func (p *Provisioner) downloadSBOM(ui packersdk.Ui, comm packersdk.Communicator) pf := io.MultiWriter(f) // Download the file - ui.Say(fmt.Sprintf("Downloading SBOM file %s => %s", src, dst)) + ui.Say(fmt.Sprintf("Downloading SBOM file %s for Packer => %s", src, dst)) if err = comm.Download(src, pf); err != nil { - ui.Error(fmt.Sprintf("download failed for SBOM file: %s", err)) + ui.Error(fmt.Sprintf("download failed for Packer SBOM file: %s", err)) return dst, err } return dst, nil } -func (p *Provisioner) compressFile(ui packersdk.Ui, filePath string) ([]byte, error) { - sourceFile, err := os.Open(filePath) +// downloadSBOMForUser downloads a SBOM from a specified source to a local +// destination given by user. It works with all communicators from packersdk. +func (p *Provisioner) downloadSBOMForUser( + ui packersdk.Ui, comm packersdk.Communicator, +) { + src, err := interpolate.Render(p.config.Source, &p.config.ctx) if err != nil { - return nil, err + ui.Say(fmt.Sprintf("error interpolating source: %s", err)) + return } - defer sourceFile.Close() - data, err := io.ReadAll(sourceFile) + // Determine the destination path + dst := p.config.Destination + if dst == "" { + ui.Say("skipped downloading SBOM file for user because 'Destination' is not provided") + return + } + + dst, err = interpolate.Render(dst, &p.config.ctx) if err != nil { - return nil, err + ui.Say(fmt.Sprintf("error interpolating SBOM file destination: %s", err)) + return + } + + 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 + } + + 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 + } + dst = tmpFile.Name() + tmpFile.Close() + } + } + + // 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 } - encoder, err := zstd.NewWriter(nil) + // Open the destination file + f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - return nil, err + ui.Say(fmt.Sprintf("failed to open destination file: %s", err)) + return } - defer encoder.Close() + defer f.Close() - compressedData := encoder.EncodeAll(data, nil) + // Create MultiWriter for the current progress + pf := io.MultiWriter(f) - ui.Say(fmt.Sprintf("SBOM file compressed successfully. Size: %d bytes", len(compressedData))) - return compressedData, nil + // 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 + } } type SBOM struct { diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go index b9adb58213f..c09747a91f4 100644 --- a/provisioner/hcp_sbom/provisioner_test.go +++ b/provisioner/hcp_sbom/provisioner_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "github.com/hashicorp/packer-plugin-sdk/packer" - "github.com/klauspost/compress/zstd" "io" "os" "testing" @@ -94,8 +93,8 @@ func TestDownloadSBOM(t *testing.T) { provisioner := &Provisioner{ config: tt.config, } - - destPath, err := provisioner.downloadSBOM(ui, comm) + generatedData := map[string]interface{}{} + destPath, err := provisioner.downloadSBOMForPacker(ui, comm, generatedData) if tt.expectError { if err == nil { t.Fatalf("expected error, got none") @@ -173,47 +172,3 @@ func TestValidateSBOM(t *testing.T) { }) } } - -func TestCompressFile(t *testing.T) { - ui := &MockUi{} - provisioner := &Provisioner{} - validSBOM := SBOM{ - BomFormat: "CycloneDX", - SpecVersion: "1.0", - } - data, _ := json.Marshal(validSBOM) - filePath := "data.json" - //os.WriteFile(filePath, data, 0644) - //defer os.Remove(filePath) - - sourceFile, err := os.Open(filePath) - if err != nil { - t.Fatalf("expected no error:%v", err) - } - defer sourceFile.Close() - - data, err = io.ReadAll(sourceFile) - if err != nil { - t.Fatalf("expected no error:%v", err) - } - - compressedData, err := provisioner.compressFile(ui, filePath) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - decoder, err := zstd.NewReader(nil) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - defer decoder.Close() - - decompressedData, err := decoder.DecodeAll(compressedData, nil) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - if string(decompressedData) != string(data) { - t.Fatalf("expected decompressed data to be '%s', got %s", data, decompressedData) - } -} From df8f3cf717b59f6615a6e055ad09ec4001d4954c Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 25 Sep 2024 17:00:42 -0400 Subject: [PATCH 04/27] Fix lint --- packer/build.go | 22 ++++------------------ packer/provisioner.go | 15 +++++++++++---- provisioner/hcp_sbom/provisioner.go | 9 +++++---- provisioner/hcp_sbom/provisioner_test.go | 3 ++- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/packer/build.go b/packer/build.go index 65b084565de..c1dac8944fb 100644 --- a/packer/build.go +++ b/packer/build.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "log" - "path/filepath" "sync" "github.com/hashicorp/packer-plugin-sdk/common" @@ -52,19 +51,13 @@ type CoreBuild struct { l sync.Mutex prepareCalled bool - SBOMFileName string - SBOMFileCompressed []byte -} - -type SBOM struct { - FileName string - CompressedData []byte + SBOMFilesCompressed [][]byte } type BuildMetadata struct { PackerVersion string Plugins map[string]PluginDetails - SBOM SBOM + SBOMs [][]byte } func (b *CoreBuild) getPluginsMetadata() map[string]PluginDetails { @@ -98,10 +91,7 @@ func (b *CoreBuild) GetMetadata() BuildMetadata { metadata := BuildMetadata{ PackerVersion: version.FormattedVersion(), Plugins: b.getPluginsMetadata(), - SBOM: SBOM{ - FileName: b.SBOMFileName, - CompressedData: b.SBOMFileCompressed, - }, + SBOMs: b.SBOMFilesCompressed, } return metadata } @@ -320,11 +310,7 @@ func (b *CoreBuild) Run(ctx context.Context, originalUi packersdk.Ui) ([]packers if !ok { continue } - b.SBOMFileName = filepath.Base(sbomInternalProvisioner.TempFileLoc) - b.SBOMFileCompressed = sbomInternalProvisioner.CompressedData - - fmt.Printf("==== SBOM File Name: %v ====\n", b.SBOMFileName) - fmt.Printf("==== SBOM Compressed Data: %v ====\n", len(sbomInternalProvisioner.CompressedData)) + b.SBOMFilesCompressed = append(b.SBOMFilesCompressed, sbomInternalProvisioner.CompressedData) } } diff --git a/packer/provisioner.go b/packer/provisioner.go index e2f7ccf791c..abd8faa88f0 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -6,10 +6,12 @@ package packer import ( "context" "fmt" - "github.com/klauspost/compress/zstd" "io" "log" "os" + + "github.com/klauspost/compress/zstd" + "time" "github.com/hashicorp/hcl/v2/hcldec" @@ -267,10 +269,16 @@ func (p *SBOMInternalProvisioner) Provision( if err != nil { return fmt.Errorf("failed to create internal temporary file for Packer SBOM: %s", err) } + defer tmpFile.Close() + defer func(name string) { + fileRemoveErr := os.Remove(name) + if fileRemoveErr != nil { + log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr) + } + }(p.TempFileLoc) + generatedData["dst"] = tmpFile.Name() p.TempFileLoc = tmpFile.Name() - ctx = context.WithValue(ctx, "sbomFilePath", tmpFile.Name()) - tmpFile.Close() err = p.Provisioner.Provision(ctx, ui, comm, generatedData) if err != nil { @@ -282,7 +290,6 @@ func (p *SBOMInternalProvisioner) Provision( return err } p.CompressedData = compressedData - os.Remove(p.TempFileLoc) return nil } diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index eff5647226d..23984ec75b3 100644 --- a/provisioner/hcp_sbom/provisioner.go +++ b/provisioner/hcp_sbom/provisioner.go @@ -11,15 +11,16 @@ import ( "encoding/json" "errors" "fmt" + "io" + "os" + "path/filepath" + "strings" + "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" - "io" - "os" - "path/filepath" - "strings" ) type Config struct { diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go index c09747a91f4..ec0a0ecfdd2 100644 --- a/provisioner/hcp_sbom/provisioner_test.go +++ b/provisioner/hcp_sbom/provisioner_test.go @@ -3,10 +3,11 @@ package hcp_sbom import ( "encoding/json" "fmt" - "github.com/hashicorp/packer-plugin-sdk/packer" "io" "os" "testing" + + "github.com/hashicorp/packer-plugin-sdk/packer" ) type MockUi struct { From 54a0f9a13bc082218bc7ca367518efea2553d7cc Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 25 Sep 2024 17:23:21 -0400 Subject: [PATCH 05/27] Fix tests --- provisioner/hcp_sbom/provisioner_test.go | 23 +++++++++++++++++++---- provisioner/hcp_sbom/test-sbom.json | 1 - 2 files changed, 19 insertions(+), 5 deletions(-) delete mode 100644 provisioner/hcp_sbom/test-sbom.json diff --git a/provisioner/hcp_sbom/provisioner_test.go b/provisioner/hcp_sbom/provisioner_test.go index ec0a0ecfdd2..ef447507269 100644 --- a/provisioner/hcp_sbom/provisioner_test.go +++ b/provisioner/hcp_sbom/provisioner_test.go @@ -31,7 +31,7 @@ func (m *MockCommunicator) Download(src string, dst io.Writer) error { return err } -func TestDownloadSBOM(t *testing.T) { +func TestDownloadSBOMForPacker(t *testing.T) { ui := &MockUi{} comm := &MockCommunicator{} @@ -54,7 +54,7 @@ func TestDownloadSBOM(t *testing.T) { Source: "mock-source/sbom.json", Destination: "test-dir/", }, - expectError: true, + expectError: false, }, { name: "Source is a json file, Destination is a json file", @@ -78,7 +78,7 @@ func TestDownloadSBOM(t *testing.T) { Source: "mock-source/sbom.json", Destination: "test-output-data", }, - expectError: true, + expectError: false, }, { name: "Source is a json file, Destination is empty", @@ -94,7 +94,22 @@ func TestDownloadSBOM(t *testing.T) { provisioner := &Provisioner{ config: tt.config, } - generatedData := map[string]interface{}{} + + 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 { diff --git a/provisioner/hcp_sbom/test-sbom.json b/provisioner/hcp_sbom/test-sbom.json deleted file mode 100644 index fc1badee633..00000000000 --- a/provisioner/hcp_sbom/test-sbom.json +++ /dev/null @@ -1 +0,0 @@ -{"bomFormat":"InvalidFormat","specVersion":"1.0"} \ No newline at end of file From d17d0a7dac2b818295180514d5e79adce5945b30 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 30 Sep 2024 14:51:08 -0400 Subject: [PATCH 06/27] Add PR suggestions --- go.mod | 5 +- go.sum | 17 ++- packer/build.go | 9 +- packer/provisioner.go | 32 ++--- provisioner/hcp_sbom/provisioner.go | 169 +++++++++++------------ provisioner/hcp_sbom/provisioner_test.go | 24 ++-- 6 files changed, 124 insertions(+), 132 deletions(-) diff --git a/go.mod b/go.mod index 85c93cff2b0..17fc554b6cc 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 3d3d78c2fd1..3a6673ea096 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 c1dac8944fb..560bcd5b59d 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 abd8faa88f0..e44b48d8ba8 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) - if err != nil { - return nil, err - } - defer sourceFile.Close() - - data, err := io.ReadAll(sourceFile) + data, err := os.ReadFile(filePath) 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 23984ec75b3..ee312719413 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" + "log" "os" - "path/filepath" - "strings" + "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) } -} -type SBOM struct { - BomFormat string `json:"bomFormat"` - SpecVersion string `json:"specVersion"` + ui.Say(fmt.Sprintf("User SBOM file successfully downloaded to: %s\n", dst)) + 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 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 - } - - var sbom SBOM - if err := json.Unmarshal(data, &sbom); err != nil { - return fmt.Errorf("failed to unmarshal JSON: %w", 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) } - if sbom.BomFormat != "CycloneDX" { - return fmt.Errorf("invalid bomFormat: %s", sbom.BomFormat) + if bom.BOMFormat != "CycloneDX" { + return fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.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 ef447507269..8509bea701f 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 ed7a0d5f429fdbc8276e9fe6c9a0c7eb65995691 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 30 Sep 2024 15:00:11 -0400 Subject: [PATCH 07/27] 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 8509bea701f..88ff6d8f6d7 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 66daeb7c50aa825e3aa1e34258a67b8f2fc33393 Mon Sep 17 00:00:00 2001 From: Devashish Date: Fri, 4 Oct 2024 14:01:57 -0400 Subject: [PATCH 08/27] 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 a8019fbdef1..a46cec04b14 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 0cb7e7a8092..936c435f6bc 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 b38d84484d74c327e1d0ece6835cfca0ec382818 Mon Sep 17 00:00:00 2001 From: Devashish Date: Tue, 8 Oct 2024 21:56:50 -0400 Subject: [PATCH 09/27] DRY download SBOM functions --- provisioner/hcp_sbom/provisioner.go | 106 +++++++++++++++------------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/provisioner/hcp_sbom/provisioner.go b/provisioner/hcp_sbom/provisioner.go index ee312719413..db906c83a26 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") + // Attempt to download SBOM for User + dst, err := p.getUserDestination() + if err != nil { + return "", fmt.Errorf("failed to determine user SBOM destination: %s", err) } - // 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) + // 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)) } - // Open the destination file - f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + // Attempt to download SBOM for Packer + dst, err = p.getPackerDestination(generatedData) if err != nil { - return dst, fmt.Errorf("failed to open destination file for Packer SBOM: %s", err) + return "", fmt.Errorf("failed to get Packer SBOM destination: %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 + 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 2b3c440d55ad6b9998383a4bfaa89a3423d69c44 Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 9 Oct 2024 10:25:30 -0400 Subject: [PATCH 10/27] 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 17fc554b6cc..b24877be722 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 3a6673ea096..38d4e0a5bd6 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 560bcd5b59d..313fb0d382c 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 e44b48d8ba8..7e15ee80eb3 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 db906c83a26..0c6d9925d71 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 88ff6d8f6d7..00000000000 --- 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 4139f75f32bb7c9694518007426728e0e020497c Mon Sep 17 00:00:00 2001 From: Devashish Date: Wed, 9 Oct 2024 10:29:46 -0400 Subject: [PATCH 11/27] 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 313fb0d382c..e8d8e865112 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 7e15ee80eb3..df63a9ec800 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 0c6d9925d71..5b48c58cd96 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 4cf3811980ad683c41f2bd6363cdb5e5dcb381ba Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 14 Oct 2024 17:18:05 -0400 Subject: [PATCH 12/27] 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 e8d8e865112..eade2625dd6 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 df63a9ec800..fcea43a64af 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 5b48c58cd96..6eae46a805f 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 00000000000..f6c554a53b5 --- /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 768b9a0c3216da652da601fe2f43e9fe6e4dd844 Mon Sep 17 00:00:00 2001 From: Devashish Date: Tue, 15 Oct 2024 14:56:43 -0400 Subject: [PATCH 13/27] 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 308c159c333..1e303858d61 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 49ca8872912..1f909aca3df 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 66e41a7e84d..9d574294cec 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 fcea43a64af..1bdee785dd2 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 6eae46a805f..1f6555862a6 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 a46cec04b14..eb241a06c77 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 936c435f6bc..2f227c2b0ff 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 890b476c18fd1aa8d8bee8d53a52a5907e8e3ac9 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 21 Oct 2024 16:30:48 -0400 Subject: [PATCH 14/27] 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 1bdee785dd2..24d670950a1 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 f6c554a53b5..35b6299bb9a 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 fb3d15c12cb01d59a6f36ebd5fb3bcef56906341 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 21 Oct 2024 16:54:02 -0400 Subject: [PATCH 15/27] 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 1f6555862a6..cddb9b1f29b 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 03a71420eabcf3ba11c18733aed91958e0747c40 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 21 Oct 2024 17:03:44 -0400 Subject: [PATCH 16/27] 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 cddb9b1f29b..2815c665704 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 35b6299bb9a..372d6150798 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" ) From d35cafecbb8b06d1c30e6f962147093ccd8fda5f Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Tue, 1 Oct 2024 10:55:32 -0400 Subject: [PATCH 17/27] packer_test: add file checker Some tests will create files and directories as part of the execution path for Packer, and we need a way to check this, so this commit adds a new file gadget to do those checks after a command executes. --- packer_test/common/check/file_gadgets.go | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 packer_test/common/check/file_gadgets.go diff --git a/packer_test/common/check/file_gadgets.go b/packer_test/common/check/file_gadgets.go new file mode 100644 index 00000000000..cb15f94e02d --- /dev/null +++ b/packer_test/common/check/file_gadgets.go @@ -0,0 +1,35 @@ +package check + +import ( + "fmt" + "os" +) + +type fileExists struct { + filepath string + isDir bool +} + +func (fe fileExists) Check(_, _ string, _ error) error { + st, err := os.Stat(fe.filepath) + if err != nil { + return fmt.Errorf("failed to stat %q: %s", fe.filepath, err) + } + + if st.IsDir() && !fe.isDir { + return fmt.Errorf("file %q is a directory, wasn't supposed to be", fe.filepath) + } + + if !st.IsDir() && fe.isDir { + return fmt.Errorf("file %q is not a directory, was supposed to be", fe.filepath) + } + + return nil +} + +func FileExists(filePath string, isDir bool) Checker { + return fileExists{ + filepath: filePath, + isDir: isDir, + } +} From 8a996bbed4bbf6ad537635834a9956f7185b1fe1 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 25 Oct 2024 10:39:38 -0400 Subject: [PATCH 18/27] simplify error typing for SBOM validation --- provisioner/hcp-sbom/validate.go | 47 ++++++++++++-------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/provisioner/hcp-sbom/validate.go b/provisioner/hcp-sbom/validate.go index 372d6150798..cad502cb934 100644 --- a/provisioner/hcp-sbom/validate.go +++ b/provisioner/hcp-sbom/validate.go @@ -3,6 +3,7 @@ package hcp_sbom import ( "bytes" "fmt" + "strings" "github.com/CycloneDX/cyclonedx-go" spdxjson "github.com/spdx/tools-golang/json" @@ -10,22 +11,13 @@ import ( "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 + Err error } func (e *ValidationError) Error() string { - return fmt.Sprintf(" %s error: %v", e.Type, e.Err) + return e.Err.Error() } func (e *ValidationError) Unwrap() error { @@ -37,22 +29,17 @@ func ValidateCycloneDX(content io.Reader) error { decoder := cyclonedx.NewBOMDecoder(content, cyclonedx.BOMFileFormatJSON) bom := new(cyclonedx.BOM) if err := decoder.Decode(bom); err != nil { - return &ValidationError{ - Type: ParsingErr, - Err: fmt.Errorf("error parsing CycloneDX SBOM: %w", err), - } + return fmt.Errorf("error parsing CycloneDX SBOM: %w", err) } - if bom.BOMFormat != "CycloneDX" { + if !strings.EqualFold(bom.BOMFormat, "CycloneDX") { return &ValidationError{ - Type: ValidationErr, - Err: fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat), + Err: fmt.Errorf("invalid bomFormat: %q, expected CycloneDX", bom.BOMFormat), } } if bom.SpecVersion.String() == "" { return &ValidationError{ - Type: ValidationErr, - Err: fmt.Errorf("specVersion is required"), + Err: fmt.Errorf("specVersion is required"), } } @@ -63,16 +50,12 @@ func ValidateCycloneDX(content io.Reader) error { func ValidateSPDX(content io.Reader) error { doc, err := spdxjson.Read(content) if err != nil { - return &ValidationError{ - Type: ParsingErr, - Err: fmt.Errorf("error parsing SPDX JSON file: %w", err), - } + return fmt.Errorf("error parsing SPDX JSON file: %w", err) } if doc.SPDXVersion == "" { return &ValidationError{ - Type: ValidationErr, - Err: fmt.Errorf("missing SPDXVersion"), + Err: fmt.Errorf("missing SPDXVersion"), } } @@ -92,8 +75,10 @@ func ValidateSBOM(content io.Reader) (string, error) { spdxErr := ValidateSPDX(reader) if spdxErr == nil { return "spdx", nil - } else if vErr, ok := spdxErr.(*ValidationError); ok && vErr.Type == ValidationErr { - return "", spdxErr + } + + if vErr, ok := spdxErr.(*ValidationError); ok { + return "", vErr } // Reset the reader's position @@ -104,8 +89,10 @@ func ValidateSBOM(content io.Reader) (string, error) { cycloneDxErr := ValidateCycloneDX(reader) if cycloneDxErr == nil { return "cyclonedx", nil - } else if vErr, ok := cycloneDxErr.(*ValidationError); ok && vErr.Type == ValidationErr { - return "", spdxErr + } + + if vErr, ok := cycloneDxErr.(*ValidationError); ok { + return "", vErr } return "", fmt.Errorf("error validating SBOM file: invalid SBOM format") From 4e8affb012b83d5bfcccd305e00ef4081f1c9a68 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 25 Oct 2024 11:32:06 -0400 Subject: [PATCH 19/27] fixme: add path/home to commands for docker to run, should be generalised --- packer_test/common/commands.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packer_test/common/commands.go b/packer_test/common/commands.go index 869a6b27811..52681f6c375 100644 --- a/packer_test/common/commands.go +++ b/packer_test/common/commands.go @@ -45,6 +45,8 @@ func (ts *PackerTestSuite) PackerCommand() *packerCommand { // make them as self-contained and quick as possible. // Removing telemetry here is probably for the best. "CHECKPOINT_DISABLE": "1", + "HOME": os.Getenv("HOME"), + "PATH": os.Getenv("PATH"), }, t: ts.T(), } From 127d6258755ad74421a7b90714b94d5c1f29a7e6 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 25 Oct 2024 11:32:36 -0400 Subject: [PATCH 20/27] packer_test: add intergation tests for hcp-sbom --- .../hcp-sbom/priovisioner_test.go | 85 +++++++++++++++++++ .../provisioner_tests/hcp-sbom/suite_test.go | 23 +++++ .../dest_is_file_no_interm_dirs.pkr.hcl | 36 ++++++++ .../hcp-sbom/templates/source_is_dir.pkr.hcl | 21 +++++ .../templates/source_not_existing.pkr.hcl | 21 +++++ 5 files changed, 186 insertions(+) create mode 100644 packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go create mode 100644 packer_test/provisioner_tests/hcp-sbom/suite_test.go create mode 100644 packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl create mode 100644 packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl create mode 100644 packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl diff --git a/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go new file mode 100644 index 00000000000..370f4d5c00a --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go @@ -0,0 +1,85 @@ +package plugin_tests + +import ( + "os" + + "github.com/hashicorp/packer/packer_test/common/check" +) + +func (ts *PackerHCPSbomTestSuite) TestSourceNotExisting() { + ts.SkipNoAcc() + + dir := ts.MakePluginDir() + defer dir.Cleanup() + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("plugins", "install", "github.com/hashicorp/docker"). + Assert(check.MustSucceed()) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("build", "templates/source_not_existing.pkr.hcl"). + Assert(check.MustFail(), check.Grep("download failed for SBOM file")) +} + +// Greayed out because the communicator for the docker plugin does not return an error +// when downloading a full directory, instead it returns a 0-byte stream without an error. +// +// So the sbom provisioner fails with a validation error instead of a file not found type +// of error. +// +// func (ts *PackerHCPSbomTestSuite) TestSourceIsDir() { +// ts.SkipNoAcc() +// +// path, cleanup := ts.MakePluginDir() +// defer cleanup() +// +// ts.PackerCommand().UsePluginDir(path). +// SetArgs("plugins", "install", "github.com/hashicorp/docker"). +// Assert(check.MustSucceed()) +// +// ts.PackerCommand().UsePluginDir(path). +// SetArgs("build", "templates/source_is_dir.pkr.hcl"). +// Assert(check.MustFail(), check.Grep("download failed for SBOM file"), check.Dump(ts.T())) +// } + +// * output file - does not exist, and intermediate dirs don't exist +func (ts *PackerHCPSbomTestSuite) TestDestFile_NoIntermediateDirs() { + ts.SkipNoAcc() + + dir := ts.MakePluginDir() + defer dir.Cleanup() + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("plugins", "install", "github.com/hashicorp/docker"). + Assert(check.MustSucceed()) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("build", "./templates/dest_is_file_no_interm_dirs.pkr.hcl"). + Assert(check.MustSucceed(), check.FileExists("sbom/sbom_cyclonedx", false)) + + os.RemoveAll("sbom") +} + +// * output file - does not exist, and intermediate dirs already exist +func (ts *PackerHCPSbomTestSuite) TestDestFile_WithIntermediateDirs() { + ts.SkipNoAcc() + + dir := ts.MakePluginDir() + defer dir.Cleanup() + + os.MkdirAll("sbom", 0755) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("plugins", "install", "github.com/hashicorp/docker"). + Assert(check.MustSucceed()) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("build", "./templates/dest_is_file_no_interm_dirs.pkr.hcl"). + Assert(check.MustSucceed(), check.FileExists("sbom/sbom_cyclonedx", false)) + + os.RemoveAll("sbom") +} + +// * output directory (without trailing slash) - directory exists +// * output directory (with trailing slash) - directory exists +// * output directory (with trailing slash) - directory doesn't exist diff --git a/packer_test/provisioner_tests/hcp-sbom/suite_test.go b/packer_test/provisioner_tests/hcp-sbom/suite_test.go new file mode 100644 index 00000000000..a3855ebb660 --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/suite_test.go @@ -0,0 +1,23 @@ +package plugin_tests + +import ( + "testing" + + "github.com/hashicorp/packer/packer_test/common" + "github.com/stretchr/testify/suite" +) + +type PackerHCPSbomTestSuite struct { + *common.PackerTestSuite +} + +func Test_PackerPluginSuite(t *testing.T) { + baseSuite, cleanup := common.InitBaseSuite(t) + defer cleanup() + + ts := &PackerHCPSbomTestSuite{ + baseSuite, + } + + suite.Run(t, ts) +} diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl new file mode 100644 index 00000000000..d3441a96bcb --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl @@ -0,0 +1,36 @@ +packer { + required_plugins { + docker = { + version = ">= 1.0.0" + source = "github.com/hashicorp/docker" + } + } +} + +source "docker" "ubuntu" { + image = "ubuntu:20.04" + commit = true +} + +build { + sources = ["source.docker.ubuntu"] + + provisioner "shell" { + inline = [ + "apt-get update -y", + "apt-get install -y curl", + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" + ] + } + + provisioner "shell" { + inline = [ + "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", + ] + } + + provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom/sbom_cyclonedx" + } +} diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl new file mode 100644 index 00000000000..65cabda3de2 --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl @@ -0,0 +1,21 @@ +packer { + required_plugins { + docker = { + version = ">= 1.0.0" + source = "github.com/hashicorp/docker" + } + } +} + +source "docker" "ubuntu" { + image = "ubuntu:20.04" + commit = true +} + +build { + sources = ["source.docker.ubuntu"] + + provisioner "hcp-sbom" { + source = "/tmp" + } +} diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl new file mode 100644 index 00000000000..fc9a54f2f17 --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl @@ -0,0 +1,21 @@ +packer { + required_plugins { + docker = { + version = ">= 1.0.0" + source = "github.com/hashicorp/docker" + } + } +} + +source "docker" "ubuntu" { + image = "ubuntu:20.04" + commit = true +} + +build { + sources = ["source.docker.ubuntu"] + + provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + } +} From 9dae54addd24145b07c051d069fcd1c141ffc130 Mon Sep 17 00:00:00 2001 From: Devashish Date: Fri, 25 Oct 2024 16:06:06 -0400 Subject: [PATCH 21/27] Add more acceptance tests --- packer_test/common/check/file_gadgets.go | 30 ++++++++++ .../hcp-sbom/priovisioner_test.go | 58 ++++++++++++++++++- .../hcp-sbom/templates/dest_is_dir.pkr.hcl | 36 ++++++++++++ .../dest_is_dir_with_trailing_slash.pkr.hcl | 36 ++++++++++++ .../dest_is_file_no_interm_dirs.pkr.hcl | 4 +- .../dest_is_file_with_interm_dirs.pkr.hcl | 36 ++++++++++++ .../hcp-sbom/templates/source_is_dir.pkr.hcl | 2 +- .../templates/source_not_existing.pkr.hcl | 2 +- 8 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir.pkr.hcl create mode 100644 packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir_with_trailing_slash.pkr.hcl create mode 100644 packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_with_interm_dirs.pkr.hcl diff --git a/packer_test/common/check/file_gadgets.go b/packer_test/common/check/file_gadgets.go index cb15f94e02d..30bcc3a0537 100644 --- a/packer_test/common/check/file_gadgets.go +++ b/packer_test/common/check/file_gadgets.go @@ -3,6 +3,7 @@ package check import ( "fmt" "os" + "regexp" ) type fileExists struct { @@ -33,3 +34,32 @@ func FileExists(filePath string, isDir bool) Checker { isDir: isDir, } } + +type fileInDir struct { + filename string + dirPath string +} + +func (fe fileInDir) Check(_, _ string, _ error) error { + files, err := os.ReadDir(fe.dirPath) + if err != nil { + return fmt.Errorf("failed to read dir %q: %s", fe.dirPath, err) + } + + pattern := regexp.MustCompile(fe.filename) + + for _, file := range files { + if !file.IsDir() && pattern.MatchString(file.Name()) { + return nil + } + } + + return fmt.Errorf("file %q not found in dir %q", fe.filename, fe.dirPath) +} + +func FileInDir(dirPath string, filename string) Checker { + return fileInDir{ + filename: filename, + dirPath: dirPath, + } +} diff --git a/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go index 370f4d5c00a..43c15c18252 100644 --- a/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go +++ b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go @@ -55,7 +55,7 @@ func (ts *PackerHCPSbomTestSuite) TestDestFile_NoIntermediateDirs() { ts.PackerCommand().UsePluginDir(dir). SetArgs("build", "./templates/dest_is_file_no_interm_dirs.pkr.hcl"). - Assert(check.MustSucceed(), check.FileExists("sbom/sbom_cyclonedx", false)) + Assert(check.MustSucceed(), check.FileExists("sbom/sbom_cyclonedx.json", false)) os.RemoveAll("sbom") } @@ -75,11 +75,65 @@ func (ts *PackerHCPSbomTestSuite) TestDestFile_WithIntermediateDirs() { ts.PackerCommand().UsePluginDir(dir). SetArgs("build", "./templates/dest_is_file_no_interm_dirs.pkr.hcl"). - Assert(check.MustSucceed(), check.FileExists("sbom/sbom_cyclonedx", false)) + Assert(check.MustSucceed(), check.FileExists("sbom/sbom_cyclonedx.json", false)) os.RemoveAll("sbom") } // * output directory (without trailing slash) - directory exists +func (ts *PackerHCPSbomTestSuite) TestDestDir_NoTrailingSlash() { + ts.SkipNoAcc() + + dir := ts.MakePluginDir() + defer dir.Cleanup() + + os.MkdirAll("sbom", 0755) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("plugins", "install", "github.com/hashicorp/docker"). + Assert(check.MustSucceed()) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("build", "./templates/dest_is_dir.pkr.hcl"). + Assert(check.MustSucceed(), check.FileInDir("sbom", "packer-user-sbom-.*.json")) + + os.RemoveAll("sbom") +} + // * output directory (with trailing slash) - directory exists +func (ts *PackerHCPSbomTestSuite) TestDestDir_WithTrailingSlash() { + ts.SkipNoAcc() + + dir := ts.MakePluginDir() + defer dir.Cleanup() + + os.MkdirAll("sbom", 0755) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("plugins", "install", "github.com/hashicorp/docker"). + Assert(check.MustSucceed()) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("build", "./templates/dest_is_dir_with_trailing_slash.pkr.hcl"). + Assert(check.MustSucceed(), check.FileInDir("sbom", "packer-user-sbom-.*.json")) + + os.RemoveAll("sbom") +} + // * output directory (with trailing slash) - directory doesn't exist +func (ts *PackerHCPSbomTestSuite) TestDestDir_WithTrailingSlash_NoDir() { + ts.SkipNoAcc() + + dir := ts.MakePluginDir() + defer dir.Cleanup() + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("plugins", "install", "github.com/hashicorp/docker"). + Assert(check.MustSucceed()) + + ts.PackerCommand().UsePluginDir(dir). + SetArgs("build", "./templates/dest_is_dir_with_trailing_slash.pkr.hcl"). + Assert(check.MustFail()) + + os.RemoveAll("sbom") +} diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir.pkr.hcl new file mode 100644 index 00000000000..1a405a50bee --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir.pkr.hcl @@ -0,0 +1,36 @@ +packer { + required_plugins { + docker = { + version = ">= 1.0.0" + source = "github.com/hashicorp/docker" + } + } +} + +source "docker" "ubuntu" { + image = "ubuntu:20.04" + commit = true +} + +build { + sources = ["source.docker.ubuntu"] + + provisioner "shell" { + inline = [ + "apt-get update -y", + "apt-get install -y curl", + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" + ] + } + + provisioner "shell" { + inline = [ + "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", + ] + } + + provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom" + } +} diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir_with_trailing_slash.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir_with_trailing_slash.pkr.hcl new file mode 100644 index 00000000000..9d9ca4506b1 --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_dir_with_trailing_slash.pkr.hcl @@ -0,0 +1,36 @@ +packer { + required_plugins { + docker = { + version = ">= 1.0.0" + source = "github.com/hashicorp/docker" + } + } +} + +source "docker" "ubuntu" { + image = "ubuntu:20.04" + commit = true +} + +build { + sources = ["source.docker.ubuntu"] + + provisioner "shell" { + inline = [ + "apt-get update -y", + "apt-get install -y curl", + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" + ] + } + + provisioner "shell" { + inline = [ + "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", + ] + } + + provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom/" + } +} diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl index d3441a96bcb..9d4bcb2daec 100644 --- a/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl +++ b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_no_interm_dirs.pkr.hcl @@ -30,7 +30,7 @@ build { } provisioner "hcp-sbom" { - source = "/tmp/sbom_cyclonedx.json" - destination = "./sbom/sbom_cyclonedx" + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom/sbom_cyclonedx.json" } } diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_with_interm_dirs.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_with_interm_dirs.pkr.hcl new file mode 100644 index 00000000000..37ccbcc3b60 --- /dev/null +++ b/packer_test/provisioner_tests/hcp-sbom/templates/dest_is_file_with_interm_dirs.pkr.hcl @@ -0,0 +1,36 @@ +packer { + required_plugins { + docker = { + version = ">= 1.0.0" + source = "github.com/hashicorp/docker" + } + } +} + +source "docker" "ubuntu" { + image = "ubuntu:20.04" + commit = true +} + +build { + sources = ["source.docker.ubuntu"] + + provisioner "shell" { + inline = [ + "apt-get update -y", + "apt-get install -y curl", + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" + ] + } + + provisioner "shell" { + inline = [ + "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", + ] + } + + provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom/sbom_cyclonedx" + } +} diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl index 65cabda3de2..02522488d52 100644 --- a/packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl +++ b/packer_test/provisioner_tests/hcp-sbom/templates/source_is_dir.pkr.hcl @@ -16,6 +16,6 @@ build { sources = ["source.docker.ubuntu"] provisioner "hcp-sbom" { - source = "/tmp" + source = "/tmp" } } diff --git a/packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl b/packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl index fc9a54f2f17..a66b9968501 100644 --- a/packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl +++ b/packer_test/provisioner_tests/hcp-sbom/templates/source_not_existing.pkr.hcl @@ -16,6 +16,6 @@ build { sources = ["source.docker.ubuntu"] provisioner "hcp-sbom" { - source = "/tmp/sbom_cyclonedx.json" + source = "/tmp/sbom_cyclonedx.json" } } From b6971008ef744c45ea2c5ccdf843a15203035385 Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 28 Oct 2024 12:49:01 -0400 Subject: [PATCH 22/27] Refactor to filepath.Glob --- packer_test/common/check/file_gadgets.go | 31 +++++++------------ .../hcp-sbom/priovisioner_test.go | 4 +-- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packer_test/common/check/file_gadgets.go b/packer_test/common/check/file_gadgets.go index 30bcc3a0537..43ccc89e25e 100644 --- a/packer_test/common/check/file_gadgets.go +++ b/packer_test/common/check/file_gadgets.go @@ -3,7 +3,7 @@ package check import ( "fmt" "os" - "regexp" + "path/filepath" ) type fileExists struct { @@ -35,31 +35,24 @@ func FileExists(filePath string, isDir bool) Checker { } } -type fileInDir struct { - filename string - dirPath string +type fileGlob struct { + filepath string } -func (fe fileInDir) Check(_, _ string, _ error) error { - files, err := os.ReadDir(fe.dirPath) +func (fe fileGlob) Check(_, _ string, _ error) error { + matches, err := filepath.Glob(fe.filepath) if err != nil { - return fmt.Errorf("failed to read dir %q: %s", fe.dirPath, err) + return fmt.Errorf("error finding file %q: %q", fe.filepath, err) } - pattern := regexp.MustCompile(fe.filename) - - for _, file := range files { - if !file.IsDir() && pattern.MatchString(file.Name()) { - return nil - } + if len(matches) == 0 { + return fmt.Errorf("file %q not found", fe.filepath) } - - return fmt.Errorf("file %q not found in dir %q", fe.filename, fe.dirPath) + return nil } -func FileInDir(dirPath string, filename string) Checker { - return fileInDir{ - filename: filename, - dirPath: dirPath, +func FileGlob(filename string) Checker { + return fileGlob{ + filepath: filename, } } diff --git a/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go index 43c15c18252..46d40f33b26 100644 --- a/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go +++ b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go @@ -95,7 +95,7 @@ func (ts *PackerHCPSbomTestSuite) TestDestDir_NoTrailingSlash() { ts.PackerCommand().UsePluginDir(dir). SetArgs("build", "./templates/dest_is_dir.pkr.hcl"). - Assert(check.MustSucceed(), check.FileInDir("sbom", "packer-user-sbom-.*.json")) + Assert(check.MustSucceed(), check.FileGlob("./sbom/packer-user-sbom-*.json")) os.RemoveAll("sbom") } @@ -115,7 +115,7 @@ func (ts *PackerHCPSbomTestSuite) TestDestDir_WithTrailingSlash() { ts.PackerCommand().UsePluginDir(dir). SetArgs("build", "./templates/dest_is_dir_with_trailing_slash.pkr.hcl"). - Assert(check.MustSucceed(), check.FileInDir("sbom", "packer-user-sbom-.*.json")) + Assert(check.MustSucceed(), check.FileGlob("./sbom/packer-user-sbom-*.json")) os.RemoveAll("sbom") } From eda9f4fc18719dc82887e116c87fa3bfc49edd7d Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 28 Oct 2024 14:04:22 -0400 Subject: [PATCH 23/27] fix: file download logic when given destination does not exist --- .../hcp-sbom/priovisioner_test.go | 2 +- provisioner/hcp-sbom/provisioner.go | 38 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go index 46d40f33b26..0d82c6441e7 100644 --- a/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go +++ b/packer_test/provisioner_tests/hcp-sbom/priovisioner_test.go @@ -133,7 +133,7 @@ func (ts *PackerHCPSbomTestSuite) TestDestDir_WithTrailingSlash_NoDir() { ts.PackerCommand().UsePluginDir(dir). SetArgs("build", "./templates/dest_is_dir_with_trailing_slash.pkr.hcl"). - Assert(check.MustFail()) + Assert(check.MustSucceed(), check.FileGlob("./sbom/packer-user-sbom-*.json")) os.RemoveAll("sbom") } diff --git a/provisioner/hcp-sbom/provisioner.go b/provisioner/hcp-sbom/provisioner.go index 2815c665704..2fd9e834e98 100644 --- a/provisioner/hcp-sbom/provisioner.go +++ b/provisioner/hcp-sbom/provisioner.go @@ -168,19 +168,35 @@ func (p *Provisioner) getUserDestination() (string, error) { // 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) + if err == nil { + 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) } - } else { - return "", fmt.Errorf("failed to stat destination for user SBOM: %s\n", err) + dst = tmpFile.Name() + tmpFile.Close() } - } 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") + return dst, nil + } + + outDir := filepath.Dir(dst) + // In case the destination does not exist, we'll get the dirpath, + // and create it if it doesn't already exist + err = os.MkdirAll(outDir, 0755) + if err != nil { + return "", fmt.Errorf("failed to create destination directory for user SBOM: %s\n", err) + } + + // Check if the destination is a directory after the previous step. + // + // This happens if the path specified ends with a `/`, in which case the + // destination is a directory, and we must create a temporary file in + // this destination directory. + destStat, statErr := os.Stat(dst) + if statErr == nil && destStat.IsDir() { + tmpFile, err := os.CreateTemp(outDir, "packer-user-sbom-*.json") if err != nil { return "", fmt.Errorf("failed to create temporary file in user SBOM directory %s: %s", dst, err) } From 85057915bd102beaebc15d257028ca8feac339c6 Mon Sep 17 00:00:00 2001 From: Devashish Date: Thu, 31 Oct 2024 16:34:27 -0400 Subject: [PATCH 24/27] Fix error messages for FileGlob checker --- packer_test/common/check/file_gadgets.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packer_test/common/check/file_gadgets.go b/packer_test/common/check/file_gadgets.go index 43ccc89e25e..7f3a12bb32e 100644 --- a/packer_test/common/check/file_gadgets.go +++ b/packer_test/common/check/file_gadgets.go @@ -42,12 +42,13 @@ type fileGlob struct { func (fe fileGlob) Check(_, _ string, _ error) error { matches, err := filepath.Glob(fe.filepath) if err != nil { - return fmt.Errorf("error finding file %q: %q", fe.filepath, err) + return fmt.Errorf("error evaluating file glob pattern %q: %v", fe.filepath, err) } if len(matches) == 0 { - return fmt.Errorf("file %q not found", fe.filepath) + return fmt.Errorf("no matches found for glob expression %q", fe.filepath) } + return nil } From cf02850fcddc6a8de373616c72305e127b9ae6ac Mon Sep 17 00:00:00 2001 From: Devashish Date: Thu, 31 Oct 2024 16:56:35 -0400 Subject: [PATCH 25/27] parity in error messages --- packer_test/common/check/file_gadgets.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packer_test/common/check/file_gadgets.go b/packer_test/common/check/file_gadgets.go index 7f3a12bb32e..83d507c4e62 100644 --- a/packer_test/common/check/file_gadgets.go +++ b/packer_test/common/check/file_gadgets.go @@ -46,7 +46,7 @@ func (fe fileGlob) Check(_, _ string, _ error) error { } if len(matches) == 0 { - return fmt.Errorf("no matches found for glob expression %q", fe.filepath) + return fmt.Errorf("no matches found for file glob pattern %q", fe.filepath) } return nil From b446bceee30b53afaefd81733e596074a2995573 Mon Sep 17 00:00:00 2001 From: Devashish Date: Fri, 1 Nov 2024 12:34:55 -0400 Subject: [PATCH 26/27] Add docs --- website/content/community-plugins.mdx | 1 + .../content/docs/provisioners/hcp-sbom.mdx | 161 ++++++++++++++++++ website/content/docs/provisioners/index.mdx | 1 + website/data/docs-nav-data.json | 4 + 4 files changed, 167 insertions(+) create mode 100644 website/content/docs/provisioners/hcp-sbom.mdx diff --git a/website/content/community-plugins.mdx b/website/content/community-plugins.mdx index fa245b73dfd..43a427c3f95 100644 --- a/website/content/community-plugins.mdx +++ b/website/content/community-plugins.mdx @@ -24,6 +24,7 @@ HashiCorp maintainers for advice on how to get started contributing. ## Provisioners - File +- HCP SBOM - InSpec - PowerShell - Shell diff --git a/website/content/docs/provisioners/hcp-sbom.mdx b/website/content/docs/provisioners/hcp-sbom.mdx new file mode 100644 index 00000000000..e49875f896f --- /dev/null +++ b/website/content/docs/provisioners/hcp-sbom.mdx @@ -0,0 +1,161 @@ +--- +description: | + The `hcp-sbom` Packer provisioner downloads the SBOM file from remote and + sends to HCP Packer when build is done. +page_title: HCP SBOM - Provisioners +--- + + + + + +# HCP SBOM Provisioner + +Type: `hcp-sbom` + +The `hcp-sbom` Packer provisioner downloads the SBOM file from the remote machine +and sends it to HCP Packer when the build is complete (only if the template is +HCP-enabled). The SBOM file is automatically removed at the end of the process. +If the user wants to retain a copy of the SBOM file, they should specify the +`destination` field in the provisioner. + +Currently, we support `CycloneDX` and `SPDX` SBOM formats in `JSON`. + +TBA: Add more details about max number of files allowed to download, and + if we are going to add the file name field. + +## Basic Example + + + + +```json +{ + "type": "hcp-sbom", + "source": "/tmp/sbom_cyclonedx.json", + "destination": "./sbom/sbom_cyclonedx.json" +} +``` + + + + +```hcl +provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom/sbom_cyclonedx.json" +} +``` + + + + +## Configuration Reference + +Required Parameters: + +@include 'provisioner/hcp-sbom/Config-required.mdx' + +Optional Parameters: + +@include '/provisioner/hcp-sbom/Config-not-required.mdx' + +## Example Usage + + + + +```json +{ + "builders": [ + { + "type": "docker", + "image": "ubuntu:20.04", + "commit": true + } + ], + "provisioners": [ + { + "type": "shell", + "inline": [ + "apt-get update -y", + "apt-get install -y curl", + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" + ] + }, + { + "type": "shell", + "inline": [ + "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", + "cnquery sbom --output spdx-json --output-target /tmp/sbom_spdx.json" + ] + }, + { + "type": "hcp-sbom", + "source": "/tmp/sbom_cyclonedx.json", + "destination": "./sbom" + }, + { + "type": "hcp-sbom", + "source": "/tmp/sbom_spdx.json", + "destination": "./sbom/sbom_spdx.json" + } + ] +} +``` + + + + +```hcl +packer { + required_plugins { + docker = { + version = ">= 1.0.0" + source = "github.com/hashicorp/docker" + } + } +} + +source "docker" "ubuntu" { + image = "ubuntu:20.04" + commit = true +} + +build { + sources = ["source.docker.ubuntu"] + + hcp_packer_registry { + bucket_name = "test-bucket" + } + + + provisioner "shell" { + inline = [ + "apt-get update -y", + "apt-get install -y curl gpg", + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" + ] + } + + provisioner "shell" { + inline = [ + "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", + "cnquery sbom --output spdx-json --output-target /tmp/sbom_spdx.json", + ] + } + + provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom" + } + + provisioner "hcp-sbom" { + source = "/tmp/sbom_spdx.json" + destination = "./sbom/sbom_spdx.json" + } +} +``` + + + diff --git a/website/content/docs/provisioners/index.mdx b/website/content/docs/provisioners/index.mdx index e6144beaef4..6be0124557f 100644 --- a/website/content/docs/provisioners/index.mdx +++ b/website/content/docs/provisioners/index.mdx @@ -20,6 +20,7 @@ The following provisioners are included with Packer: - [Breakpoint](/packer/docs/provisioners/breakpoint) - pause until the user presses `Enter` to resume a build. - [File](/packer/docs/provisioners/file) - upload files to machines image during a build. +- [HCP SBOM](/packer/docs/provisioners/hcp-sbom) - download SBOM file to machines and send to HCP Packer during a build. - [Shell](/packer/docs/provisioners/shell) - run shell scripts on the machines image during a build. - [Local Shell](/packer/docs/provisioners/shell-local) - run shell scripts on the host running Packer during a build. diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 0a7242a98ab..1e03b850d4b 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -780,6 +780,10 @@ "title": "File", "path": "provisioners/file" }, + { + "title": "HCP SBOM", + "path": "provisioners/hcp-sbom" + }, { "title": "PowerShell", "path": "provisioners/powershell" From bc70ae1f5c2a59c6065720fddcf3016b5bd5180f Mon Sep 17 00:00:00 2001 From: Devashish Date: Mon, 4 Nov 2024 11:13:20 -0500 Subject: [PATCH 27/27] Fix verbiage in website --- .../content/docs/provisioners/hcp-sbom.mdx | 125 +++++++----------- 1 file changed, 47 insertions(+), 78 deletions(-) diff --git a/website/content/docs/provisioners/hcp-sbom.mdx b/website/content/docs/provisioners/hcp-sbom.mdx index e49875f896f..a90b5bf8886 100644 --- a/website/content/docs/provisioners/hcp-sbom.mdx +++ b/website/content/docs/provisioners/hcp-sbom.mdx @@ -1,7 +1,7 @@ --- description: | - The `hcp-sbom` Packer provisioner downloads the SBOM file from remote and - sends to HCP Packer when build is done. + The `hcp-sbom` Packer provisioner downloads an SBOM file from the guest VM and + sends it to HCP Packer when the build is done. page_title: HCP SBOM - Provisioners --- @@ -13,21 +13,27 @@ page_title: HCP SBOM - Provisioners Type: `hcp-sbom` -The `hcp-sbom` Packer provisioner downloads the SBOM file from the remote machine +The `hcp-sbom` Packer provisioner downloads an SBOM file from the guest machine and sends it to HCP Packer when the build is complete (only if the template is HCP-enabled). The SBOM file is automatically removed at the end of the process. -If the user wants to retain a copy of the SBOM file, they should specify the -`destination` field in the provisioner. +If you want to retain a copy of the SBOM file, you can specify the +`destination` option in the provisioner. Currently, we support `CycloneDX` and `SPDX` SBOM formats in `JSON`. -TBA: Add more details about max number of files allowed to download, and - if we are going to add the file name field. - ## Basic Example - - + +In HCL2: + +```hcl +provisioner "hcp-sbom" { + source = "/tmp/sbom_cyclonedx.json" + destination = "./sbom/sbom_cyclonedx.json" +} +``` + +In JSON: ```json { @@ -36,19 +42,8 @@ TBA: Add more details about max number of files allowed to download, and "destination": "./sbom/sbom_cyclonedx.json" } ``` - - - -```hcl -provisioner "hcp-sbom" { - source = "/tmp/sbom_cyclonedx.json" - destination = "./sbom/sbom_cyclonedx.json" -} -``` - - ## Configuration Reference @@ -62,50 +57,8 @@ Optional Parameters: ## Example Usage - - -```json -{ - "builders": [ - { - "type": "docker", - "image": "ubuntu:20.04", - "commit": true - } - ], - "provisioners": [ - { - "type": "shell", - "inline": [ - "apt-get update -y", - "apt-get install -y curl", - "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" - ] - }, - { - "type": "shell", - "inline": [ - "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", - "cnquery sbom --output spdx-json --output-target /tmp/sbom_spdx.json" - ] - }, - { - "type": "hcp-sbom", - "source": "/tmp/sbom_cyclonedx.json", - "destination": "./sbom" - }, - { - "type": "hcp-sbom", - "source": "/tmp/sbom_spdx.json", - "destination": "./sbom/sbom_spdx.json" - } - ] -} -``` - - - +In HCL2: ```hcl packer { @@ -134,14 +87,8 @@ build { inline = [ "apt-get update -y", "apt-get install -y curl gpg", - "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"" - ] - } - - provisioner "shell" { - inline = [ + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"", "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json", - "cnquery sbom --output spdx-json --output-target /tmp/sbom_spdx.json", ] } @@ -149,13 +96,35 @@ build { source = "/tmp/sbom_cyclonedx.json" destination = "./sbom" } - - provisioner "hcp-sbom" { - source = "/tmp/sbom_spdx.json" - destination = "./sbom/sbom_spdx.json" - } } ``` - - +In JSON: + +```json +{ + "builders": [ + { + "type": "docker", + "image": "ubuntu:20.04", + "commit": true + } + ], + "provisioners": [ + { + "type": "shell", + "inline": [ + "apt-get update -y", + "apt-get install -y curl", + "bash -c \"$(curl -sSL https://install.mondoo.com/sh)\"", + "cnquery sbom --output cyclonedx-json --output-target /tmp/sbom_cyclonedx.json" + ] + }, + { + "type": "hcp-sbom", + "source": "/tmp/sbom_cyclonedx.json", + "destination": "./sbom" + } + ] +} +```