Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] Provisioner for SBOM #13171

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions command/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions hcl2template/types.packer_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions packer/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,14 @@ type CoreBuild struct {
onError string
l sync.Mutex
prepareCalled bool

SBOMFilesCompressed [][]byte
}

type BuildMetadata struct {
PackerVersion string
Plugins map[string]PluginDetails
SBOMs [][]byte
}

func (b *CoreBuild) getPluginsMetadata() map[string]PluginDetails {
Expand Down Expand Up @@ -88,6 +91,7 @@ func (b *CoreBuild) GetMetadata() BuildMetadata {
metadata := BuildMetadata{
PackerVersion: version.FormattedVersion(),
Plugins: b.getPluginsMetadata(),
SBOMs: b.SBOMFilesCompressed,
}
return metadata
}
Expand Down Expand Up @@ -300,6 +304,16 @@ func (b *CoreBuild) Run(ctx context.Context, originalUi packersdk.Ui) ([]packers
return nil, err
}

if len(b.Provisioners) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That if is superfluous, if b.Provisioners is empty, the loop won't run at all, so you can remove it I'd think

for _, p := range b.Provisioners {
sbomInternalProvisioner, ok := p.Provisioner.(*SBOMInternalProvisioner)
if !ok {
continue
}
b.SBOMFilesCompressed = append(b.SBOMFilesCompressed, sbomInternalProvisioner.CompressedData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the alternative has only one instruction, you can probably invert this logic here

Suggested change
if !ok {
continue
}
b.SBOMFilesCompressed = append(b.SBOMFilesCompressed, sbomInternalProvisioner.CompressedData)
if ok {
b.SBOMFilesCompressed = append(b.SBOMFilesCompressed, 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 {
Expand Down
7 changes: 7 additions & 0 deletions packer/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
82 changes: 82 additions & 0 deletions packer/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ package packer
import (
"context"
"fmt"
"io"
"log"
"os"

"github.com/klauspost/compress/zstd"

"time"

"github.com/hashicorp/hcl/v2/hcldec"
Expand Down Expand Up @@ -234,3 +239,80 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we keep this as part of the provisioner? Since we only create the file during Prepare and wipe it by the end, it doesn't seem that we need it here

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)
}
defer tmpFile.Close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the underlying provisioner writes to this file, we should not defer its closure I'd think, typically on Windows this might fail depending on the kind of Handle we've received from CreateTemp

defer func(name string) {
fileRemoveErr := os.Remove(name)
if fileRemoveErr != nil {
log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr)
}
}(p.TempFileLoc)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point p.TempFileLoc was not set, so this won't work I'd think, since the parameter will be empty when the function executes?


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)
if err != nil {
return err
}
p.CompressedData = compressedData
return nil
}

func (p *SBOMInternalProvisioner) compressFile(filePath string) ([]byte, error) {
sourceFile, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer sourceFile.Close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use a function like os.ReadFile here instead of manually opening/reading/closing it.


data, err := io.ReadAll(sourceFile)
if err != nil {
return nil, err
}

encoder, err := zstd.NewWriter(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest maybe setting the compression level option to SpeedBestCompression here, it seems to default to SpeedDefault, we should probably test that to see how slower it is (and it may rise depending on the payload to compress), but I would think that in this case since we're dealing with a small set of input data, we can safely aim for small output payload without a significant negative impact to the processing time.

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)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should become a log.Printf, if not removed completely imho

return compressedData, nil
}
Loading