Add support for SPDX

poc/with-api-imported
Devashish 2 years ago committed by Lucas Bajolet
parent c98a179127
commit ecb7f7e5d0

@ -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

@ -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=

@ -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)
}
}

@ -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
}

@ -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
}
}

@ -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)
}
}
})
}
}
Loading…
Cancel
Save