Merge branch 'poc/provisioner-hcp-sbom' into poc/with-api-imported

This commit is contained in:
Devashish
2024-10-21 17:32:06 -04:00
17 changed files with 439 additions and 489 deletions
+2 -2
View File
@@ -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),
+4 -1
View File
@@ -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,10 +58,12 @@ 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
github.com/shirou/gopsutil/v3 v3.23.4
github.com/spdx/tools-golang v0.5.5
)
require (
@@ -78,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
+20 -2
View File
@@ -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=
@@ -38,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=
@@ -78,6 +82,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=
@@ -495,13 +501,17 @@ 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=
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=
@@ -513,8 +523,11 @@ 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=
@@ -535,6 +548,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=
@@ -757,3 +774,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=
+1 -1
View File
@@ -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,
}
+15 -9
View File
@@ -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
}
@@ -304,13 +309,14 @@ 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 {
sbom := SBOM{
Format: sbomInternalProvisioner.SBOMFormat,
CompressedData: sbomInternalProvisioner.CompressedData,
}
b.SBOMFilesCompressed = append(b.SBOMFilesCompressed, sbomInternalProvisioner.CompressedData)
b.SBOMs = append(b.SBOMs, sbom)
}
}
+1 -1
View File
@@ -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,
}
+41 -22
View File
@@ -6,10 +6,11 @@ package packer
import (
"context"
"fmt"
"io"
"log"
"os"
hcpSbomProvisioner "github.com/hashicorp/packer/provisioner/hcp-sbom"
"github.com/klauspost/compress/zstd"
"time"
@@ -240,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
TempFileLoc string
CompressedData []byte
SBOMFormat string
}
func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() }
@@ -258,61 +261,77 @@ 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)
}
defer tmpFile.Close()
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)
sbomFormat, err := p.getSBOMFormat(tmpFile.Name())
if err != nil {
return err
}
compressedData, err := p.compressFile(tmpFile.Name())
if err != nil {
return err
}
p.CompressedData = compressedData
p.SBOMFormat = sbomFormat
return nil
}
func (p *SBOMInternalProvisioner) compressFile(filePath string) ([]byte, error) {
sourceFile, err := os.Open(filePath)
data, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
defer sourceFile.Close()
data, err := io.ReadAll(sourceFile)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
}
encoder, err := zstd.NewWriter(nil)
encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create zstd encoder: %w", err)
}
defer encoder.Close()
compressedData := encoder.EncodeAll(data, nil)
fmt.Printf(fmt.Sprintf("SBOM file compressed successfully. Size: %d bytes", len(compressedData)))
log.Printf("SBOM file compressed successfully. Size: %d bytes\n", len(compressedData))
return compressedData, nil
}
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()
format, err := hcpSbomProvisioner.ValidateSBOM(file)
if err != nil {
return "", fmt.Errorf("failed to detect SBOM format: %w", err)
}
return format, nil
}
+223
View File
@@ -0,0 +1,223 @@
// 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 (
"bytes"
"context"
"errors"
"fmt"
"io"
"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"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
// 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 {
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 {
log.Println("Starting to provision with `hcp-sbom` provisioner")
if generatedData == nil {
generatedData = make(map[string]interface{})
}
p.config.ctx.Data = generatedData
downloadErr := p.downloadAndValidateSBOM(ui, comm, generatedData)
if downloadErr != nil {
return fmt.Errorf("failed to download SBOM file: %w", downloadErr)
}
return nil
}
// 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 {
src, err := interpolate.Render(p.config.Source, &p.config.ctx)
if err != nil {
return fmt.Errorf("error interpolating SBOM source: %s", err)
}
var buf bytes.Buffer
if err = comm.Download(src, &buf); err != nil {
ui.Errorf("download failed for SBOM file: %s", err)
return err
}
reader := bytes.NewReader(buf.Bytes())
if _, err = ValidateSBOM(reader); err != nil {
ui.Errorf("validation failed for SBOM file: %s", err)
return err
}
_, err = reader.Seek(0, io.SeekStart)
if err != nil {
return err
}
// SBOM for Packer
pkrDst, err := p.getPackerDestination(generatedData)
if err != nil {
return fmt.Errorf("failed to get Packer SBOM destination: %s", err)
}
err = p.writeToFile(reader, pkrDst)
if err != nil {
return fmt.Errorf("failed to download Packer SBOM: %s", err)
}
_, 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
usrDst, err := p.getUserDestination()
if err != nil {
return fmt.Errorf("failed to determine user SBOM destination: %s", err)
}
if usrDst != "" {
err = p.writeToFile(reader, 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
}
// 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
}
dst, err := interpolate.Render(dst, &p.config.ctx)
if err != nil {
return "", fmt.Errorf("error interpolating SBOM file destination for user: %s", err)
}
// 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)
}
} else {
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)
}
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
}
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 {
return fmt.Errorf("failed to open destination file for SBOM: %s", err)
}
defer f.Close()
// Write the buffer content to the destination file
if _, err = buf.WriteTo(f); err != nil {
return err
}
return nil
}
+112
View File
@@ -0,0 +1,112 @@
package hcp_sbom
import (
"bytes"
"fmt"
"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 &ValidationError{
Type: ParsingErr,
Err: fmt.Errorf("error parsing CycloneDX SBOM: %w", err),
}
}
if bom.BOMFormat != "CycloneDX" {
return &ValidationError{
Type: ValidationErr,
Err: fmt.Errorf("invalid bomFormat: %s, expected CycloneDX", bom.BOMFormat),
}
}
if bom.SpecVersion.String() == "" {
return &ValidationError{
Type: ValidationErr,
Err: 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 &ValidationError{
Type: ParsingErr,
Err: fmt.Errorf("error parsing SPDX JSON file: %w", err),
}
}
if doc.SPDXVersion == "" {
return &ValidationError{
Type: ValidationErr,
Err: fmt.Errorf("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())
// Try validating as SPDX
spdxErr := ValidateSPDX(reader)
if spdxErr == nil {
return "spdx", nil
} else if vErr, ok := spdxErr.(*ValidationError); ok && vErr.Type == ValidationErr {
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
} else if vErr, ok := cycloneDxErr.(*ValidationError); ok && vErr.Type == ValidationErr {
return "", spdxErr
}
return "", fmt.Errorf("error validating SBOM file: invalid SBOM format")
}
-251
View File
@@ -1,251 +0,0 @@
// 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"
"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"
)
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 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)
if validationErr != nil {
return fmt.Errorf("failed to validate SBOM file: %w", validationErr)
}
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(
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(
"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 := 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)
}
// 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 for Packer => %s", src, dst))
if err = comm.Download(src, pf); err != nil {
ui.Error(fmt.Sprintf("download failed for Packer SBOM file: %s", err))
return dst, err
}
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.
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
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 {
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
}
// 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
}
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
}
}
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
}
-190
View File
@@ -1,190 +0,0 @@
package hcp_sbom
import (
"encoding/json"
"fmt"
"io"
"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)
}
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{}
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)
}
}
})
}
}
@@ -0,0 +1,13 @@
<!-- Code generated from the comments of the Config struct in provisioner/hcp-sbom/provisioner.go; DO NOT EDIT MANUALLY -->
- `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.
<!-- End of code generated from the comments of the Config struct in provisioner/hcp-sbom/provisioner.go; -->
@@ -0,0 +1,7 @@
<!-- Code generated from the comments of the Config struct in provisioner/hcp-sbom/provisioner.go; DO NOT EDIT MANUALLY -->
- `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.
<!-- End of code generated from the comments of the Config struct in provisioner/hcp-sbom/provisioner.go; -->
@@ -1,5 +0,0 @@
<!-- Code generated from the comments of the Config struct in provisioner/hcp_sbom/provisioner.go; DO NOT EDIT MANUALLY -->
- `destination` (string) - Destination
<!-- End of code generated from the comments of the Config struct in provisioner/hcp_sbom/provisioner.go; -->
@@ -1,5 +0,0 @@
<!-- Code generated from the comments of the Config struct in provisioner/hcp_sbom/provisioner.go; DO NOT EDIT MANUALLY -->
- `source` (string) - Source
<!-- End of code generated from the comments of the Config struct in provisioner/hcp_sbom/provisioner.go; -->