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