mirror of
https://github.com/hashicorp/packer.git
synced 2026-09-18 14:01:39 -04:00
Removes Syft binary download/handling from the HCP SBOM provisioner (#13636)
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/internal/sbom"
|
||||
)
|
||||
|
||||
type SBOMGenerateCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (cmd *SBOMGenerateCommand) Run(args []string) int {
|
||||
ctx, cleanup := handleTermInterrupt(cmd.Ui)
|
||||
defer cleanup()
|
||||
|
||||
cfg, ret := cmd.ParseArgs(args)
|
||||
if ret != 0 {
|
||||
return ret
|
||||
}
|
||||
return cmd.RunContext(ctx, cfg)
|
||||
}
|
||||
|
||||
func (cmd *SBOMGenerateCommand) ParseArgs(args []string) (*sbom.Config, int) {
|
||||
cfg := &sbom.Config{
|
||||
ScanPath: "/",
|
||||
Format: sbom.FormatCycloneDX, // default format
|
||||
Parallelism: 4, // default parallelism
|
||||
Scope: sbom.ScopeSquashed, // default scope
|
||||
}
|
||||
|
||||
//Parse Syft Style args
|
||||
// Parse Syft-style arguments
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
|
||||
switch arg {
|
||||
case "-o", "--output":
|
||||
// Next arg is format
|
||||
if i+1 >= len(args) {
|
||||
cmd.Ui.Error("Missing value for -o flag")
|
||||
return cfg, 1
|
||||
}
|
||||
i++
|
||||
formatStr := args[i]
|
||||
|
||||
// Parse format string
|
||||
format, err := sbom.ParseFormatFromArgs(formatStr)
|
||||
if err != nil {
|
||||
cmd.Ui.Error(err.Error())
|
||||
return cfg, 1
|
||||
}
|
||||
cfg.Format = format
|
||||
|
||||
case "--exclude":
|
||||
if i+1 >= len(args) {
|
||||
cmd.Ui.Error("Missing value for --exclude flag")
|
||||
return cfg, 1
|
||||
}
|
||||
i++
|
||||
cfg.Exclude = append(cfg.Exclude, args[i])
|
||||
|
||||
case "--scope":
|
||||
if i+1 >= len(args) {
|
||||
cmd.Ui.Error("Missing value for --scope flag")
|
||||
return cfg, 1
|
||||
}
|
||||
i++
|
||||
scope, err := sbom.ParseScopeFromArgs(args[i])
|
||||
if err != nil {
|
||||
cmd.Ui.Error(err.Error())
|
||||
return cfg, 1
|
||||
}
|
||||
cfg.Scope = scope
|
||||
|
||||
default:
|
||||
if strings.HasPrefix(arg, "--exclude=") {
|
||||
value := strings.TrimPrefix(arg, "--exclude=")
|
||||
if value == "" {
|
||||
cmd.Ui.Error("Missing value for --exclude flag")
|
||||
return cfg, 1
|
||||
}
|
||||
cfg.Exclude = append(cfg.Exclude, value)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(arg, "--scope=") {
|
||||
value := strings.TrimPrefix(arg, "--scope=")
|
||||
if value == "" {
|
||||
cmd.Ui.Error("Missing value for --scope flag")
|
||||
return cfg, 1
|
||||
}
|
||||
scope, err := sbom.ParseScopeFromArgs(value)
|
||||
if err != nil {
|
||||
cmd.Ui.Error(err.Error())
|
||||
return cfg, 1
|
||||
}
|
||||
cfg.Scope = scope
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(arg, "-") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Assume it's the scan path (positional argument)
|
||||
cfg.ScanPath = arg
|
||||
}
|
||||
}
|
||||
return cfg, 0
|
||||
}
|
||||
|
||||
func (cmd *SBOMGenerateCommand) RunContext(ctx context.Context, cfg *sbom.Config) int {
|
||||
fmt.Fprintf(os.Stderr, "Generating %s SBOM for %s...\n", cfg.Format, cfg.ScanPath)
|
||||
|
||||
// Create generator
|
||||
generator := sbom.NewGenerator(*cfg)
|
||||
|
||||
// Generate SBOM
|
||||
sbomData, err := generator.Generate(ctx)
|
||||
if err != nil {
|
||||
cmd.Ui.Error(fmt.Sprintf("SBOM generation failed: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Write to stdout (will be redirected to file via > operator)
|
||||
_, err = os.Stdout.Write(sbomData)
|
||||
if err != nil {
|
||||
cmd.Ui.Error(fmt.Sprintf("Failed to write SBOM: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "✓ SBOM generation completed")
|
||||
|
||||
return 0
|
||||
|
||||
}
|
||||
func (c *SBOMGenerateCommand) Synopsis() string {
|
||||
return "Generate SBOM for the local system (internal use)"
|
||||
}
|
||||
func (c *SBOMGenerateCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: packer sbom-generate [options] <path>
|
||||
Internal command used by the hcp-sbom provisioner.
|
||||
Options:
|
||||
-o <format> Output format: cyclonedx-json, spdx-json (default: cyclonedx-json)
|
||||
--exclude <glob> Optional: exclude path glob from scanning (repeatable)
|
||||
--scope <scope> Optional: scan scope: squashed, all-layers (default: squashed)
|
||||
Arguments:
|
||||
<path> Path to scan (default: /)
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||
"github.com/hashicorp/packer/internal/sbom"
|
||||
)
|
||||
|
||||
func TestSBOMGenerateCommand_ParseArgs_ExcludeAndScope(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
cmd := &SBOMGenerateCommand{
|
||||
Meta: Meta{
|
||||
Ui: &packersdk.BasicUi{
|
||||
Writer: &out,
|
||||
ErrorWriter: &errOut,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg, ret := cmd.ParseArgs([]string{
|
||||
"--exclude", "/tmp/**",
|
||||
"--exclude=/var/cache/**",
|
||||
"--scope", "all-layers",
|
||||
"-o", "cyclonedx-json",
|
||||
"/",
|
||||
})
|
||||
if ret != 0 {
|
||||
t.Fatalf("expected parse success, got ret=%d err=%q", ret, errOut.String())
|
||||
}
|
||||
|
||||
if cfg.Scope != sbom.ScopeAllLayers {
|
||||
t.Fatalf("expected scope %q, got %q", sbom.ScopeAllLayers, cfg.Scope)
|
||||
}
|
||||
|
||||
if len(cfg.Exclude) != 2 {
|
||||
t.Fatalf("expected 2 exclude entries, got %d", len(cfg.Exclude))
|
||||
}
|
||||
if cfg.Exclude[0] != "/tmp/**" || cfg.Exclude[1] != "/var/cache/**" {
|
||||
t.Fatalf("unexpected exclude values: %#v", cfg.Exclude)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSBOMGenerateCommand_ParseArgs_InvalidScope(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
cmd := &SBOMGenerateCommand{
|
||||
Meta: Meta{
|
||||
Ui: &packersdk.BasicUi{
|
||||
Writer: &out,
|
||||
ErrorWriter: &errOut,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, ret := cmd.ParseArgs([]string{"--scope", "bad-scope"})
|
||||
if ret == 0 {
|
||||
t.Fatalf("expected parse failure for invalid scope")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSBOMGenerateCommand_ParseArgs_UnsupportedFlagIgnored(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
cmd := &SBOMGenerateCommand{
|
||||
Meta: Meta{
|
||||
Ui: &packersdk.BasicUi{
|
||||
Writer: &out,
|
||||
ErrorWriter: &errOut,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg, ret := cmd.ParseArgs([]string{"-q", "/opt/app"})
|
||||
if ret != 0 {
|
||||
t.Fatalf("expected parse success, got ret=%d err=%q", ret, errOut.String())
|
||||
}
|
||||
|
||||
if cfg.ScanPath != "/opt/app" {
|
||||
t.Fatalf("expected scan path /opt/app, got %q", cfg.ScanPath)
|
||||
}
|
||||
|
||||
if out.Len() != 0 {
|
||||
t.Fatalf("expected no stdout output for ignored arg, got %q", out.String())
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,10 @@ func init() {
|
||||
}, nil
|
||||
},
|
||||
|
||||
"sbom-generate": func() (cli.Command, error) {
|
||||
return &command.SBOMGenerateCommand{Meta: *CommandMeta}, nil
|
||||
},
|
||||
|
||||
// plugin is essentially an alias to the plugins command
|
||||
//
|
||||
// It is not meant to be documented or used outside of simple
|
||||
|
||||
@@ -56,6 +56,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.11.0
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/anchore/syft v1.42.3
|
||||
github.com/go-openapi/strfmt v0.23.0
|
||||
github.com/oklog/ulid v1.3.1
|
||||
@@ -63,6 +64,7 @@ require (
|
||||
github.com/shirou/gopsutil/v3 v3.23.4
|
||||
github.com/spdx/tools-golang v0.5.7
|
||||
google.golang.org/grpc v1.79.3
|
||||
modernc.org/sqlite v1.46.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -83,7 +85,6 @@ require (
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.14.1 // indirect
|
||||
@@ -303,6 +304,7 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 // indirect
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
@@ -325,6 +327,7 @@ require (
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
@@ -395,6 +398,9 @@ require (
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
go 1.25.11
|
||||
|
||||
@@ -1762,14 +1762,34 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Format represents the SBOM output format
|
||||
type Format string
|
||||
|
||||
const (
|
||||
FormatCycloneDX Format = "cyclonedx"
|
||||
FormatSPDX Format = "spdx"
|
||||
ScopeSquashed string = "squashed"
|
||||
ScopeAllLayers string = "all-layers"
|
||||
)
|
||||
|
||||
// Config holds configuration for SBOM generation
|
||||
type Config struct {
|
||||
ScanPath string // Path to scan (e.g., "/", "/opt/app")
|
||||
Format Format // Output format (cyclonedx or spdx)
|
||||
Parallelism int // Number of parallel catalogers (0 = auto-detect)
|
||||
Scope string // Scan scope (squashed or all-layers)
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
// Generator generates SBOMs using embedded Syft SDK
|
||||
type Generator struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewGenerator creates a new SBOM generator with the given configuration
|
||||
func NewGenerator(cfg Config) *Generator {
|
||||
// Set defaults
|
||||
if cfg.ScanPath == "" {
|
||||
cfg.ScanPath = "/"
|
||||
}
|
||||
if cfg.Format == "" {
|
||||
cfg.Format = FormatCycloneDX
|
||||
}
|
||||
if cfg.Parallelism == 0 {
|
||||
cfg.Parallelism = 4
|
||||
}
|
||||
if cfg.Scope == "" {
|
||||
cfg.Scope = ScopeSquashed
|
||||
}
|
||||
|
||||
return &Generator{config: cfg}
|
||||
}
|
||||
|
||||
// ParseFormatFromArgs parses Syft-style format argument
|
||||
// This is a PACKAGE-LEVEL EXPORTED FUNCTION
|
||||
func ParseFormatFromArgs(formatArg string) (Format, error) {
|
||||
formatArg = strings.ToLower(formatArg)
|
||||
|
||||
if strings.Contains(formatArg, "cyclonedx") {
|
||||
return FormatCycloneDX, nil
|
||||
}
|
||||
if strings.Contains(formatArg, "spdx") {
|
||||
return FormatSPDX, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unsupported format: %s", formatArg)
|
||||
}
|
||||
|
||||
func ParseScopeFromArgs(scopeArg string) (string, error) {
|
||||
scopeArg = strings.ToLower(strings.TrimSpace(scopeArg))
|
||||
|
||||
switch scopeArg {
|
||||
case ScopeSquashed:
|
||||
return ScopeSquashed, nil
|
||||
case ScopeAllLayers, "all", "alllayers":
|
||||
return ScopeAllLayers, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported scope: %s (supported: squashed, all-layers)", scopeArg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !netbsd && !openbsd && !solaris && !mips && !mipsle && !mips64 && !(freebsd && 386)
|
||||
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/format"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// Generate creates an SBOM for the configured scan path and returns the encoded result.
|
||||
func (g *Generator) Generate(ctx context.Context) ([]byte, error) {
|
||||
sourceInput := g.config.ScanPath
|
||||
getSourceCfg := syft.DefaultGetSourceConfig()
|
||||
if len(g.config.Exclude) > 0 {
|
||||
getSourceCfg = getSourceCfg.WithExcludeConfig(source.ExcludeConfig{Paths: g.config.Exclude})
|
||||
}
|
||||
|
||||
src, err := syft.GetSource(ctx, sourceInput, getSourceCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get source: %w", err)
|
||||
}
|
||||
defer func() { _ = src.Close() }()
|
||||
|
||||
var scope source.Scope
|
||||
switch g.config.Scope {
|
||||
case ScopeAllLayers:
|
||||
scope = source.AllLayersScope
|
||||
case "", ScopeSquashed:
|
||||
scope = source.SquashedScope
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported scope: %s", g.config.Scope)
|
||||
}
|
||||
|
||||
sbomCfg := syft.DefaultCreateSBOMConfig().
|
||||
WithSearchConfig(cataloging.SearchConfig{
|
||||
Scope: scope,
|
||||
}).
|
||||
WithParallelism(g.config.Parallelism)
|
||||
|
||||
sbomResult, err := syft.CreateSBOM(ctx, src, sbomCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SBOM: %w", err)
|
||||
}
|
||||
|
||||
return g.encodeToFormat(sbomResult)
|
||||
}
|
||||
|
||||
// encodeToFormat encodes the SBOM to the requested format.
|
||||
func (g *Generator) encodeToFormat(sbomData *sbom.SBOM) ([]byte, error) {
|
||||
switch g.config.Format {
|
||||
case FormatCycloneDX:
|
||||
cfg := cyclonedxjson.DefaultEncoderConfig()
|
||||
cfg.Pretty = true
|
||||
encoder, err := cyclonedxjson.NewFormatEncoderWithConfig(
|
||||
cfg,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create CycloneDX encoder: %w", err)
|
||||
}
|
||||
return format.Encode(*sbomData, encoder)
|
||||
|
||||
case FormatSPDX:
|
||||
cfg := spdxjson.DefaultEncoderConfig()
|
||||
cfg.Pretty = true
|
||||
encoder, err := spdxjson.NewFormatEncoderWithConfig(
|
||||
cfg,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SPDX encoder: %w", err)
|
||||
}
|
||||
return format.Encode(*sbomData, encoder)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported format: %s (supported: cyclonedx, spdx)", g.config.Format)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build netbsd || openbsd || solaris || mips || mipsle || mips64 || (freebsd && 386)
|
||||
|
||||
package sbom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Generate returns an error on platforms where the Syft SDK cannot be built.
|
||||
func (g *Generator) Generate(ctx context.Context) ([]byte, error) {
|
||||
_ = ctx
|
||||
return nil, fmt.Errorf("sbom generation is not supported on %s builds", runtime.GOOS)
|
||||
}
|
||||
@@ -265,7 +265,7 @@ func wrappedMain() int {
|
||||
Args: args,
|
||||
Autocomplete: true,
|
||||
Commands: Commands,
|
||||
HelpFunc: excludeHelpFunc(Commands, []string{"execute", "plugin"}),
|
||||
HelpFunc: excludeHelpFunc(Commands, []string{"execute", "plugin", "sbom-generate"}),
|
||||
HelpWriter: os.Stdout,
|
||||
Name: "packer",
|
||||
Version: version.Version,
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcp_sbom
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
semver "github.com/Masterminds/semver/v3"
|
||||
"github.com/hashicorp/packer-plugin-sdk/retry"
|
||||
)
|
||||
|
||||
// releaseBaseURL is the base URL for downloading Packer release artifacts.
|
||||
const defaultReleaseBaseURL = "https://releases.hashicorp.com"
|
||||
|
||||
func getReleaseBaseURL() string {
|
||||
return defaultReleaseBaseURL
|
||||
}
|
||||
|
||||
// releaseIndex is the top-level structure of https://releases.hashicorp.com/packer/index.json.
|
||||
type releaseIndex struct {
|
||||
Versions map[string]releaseVersion `json:"versions"`
|
||||
}
|
||||
|
||||
// releaseVersion represents one version entry in the release index.
|
||||
type releaseVersion struct {
|
||||
Version string `json:"version"`
|
||||
Shasums string `json:"shasums"`
|
||||
Builds []releaseBuild `json:"builds"`
|
||||
}
|
||||
|
||||
// releaseBuild represents one platform build inside a release version.
|
||||
type releaseBuild struct {
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
Filename string `json:"filename"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// fetchLatestPackerVersion queries the HashiCorp releases index, sorts all
|
||||
// stable (non-prerelease) versions with semver, and returns the highest one.
|
||||
func fetchLatestPackerVersion(ctx context.Context, client *http.Client) (string, error) {
|
||||
indexURL := getReleaseBaseURL() + "/packer/index.json"
|
||||
var indexData releaseIndex
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, indexURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build index request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch release index: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP %d for %s", resp.StatusCode, indexURL)
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&indexData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve packer release index from %s: %w", indexURL, err)
|
||||
}
|
||||
|
||||
var semverList []*semver.Version
|
||||
for vStr := range indexData.Versions {
|
||||
v, parseErr := semver.NewVersion(vStr)
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
if v.Prerelease() != "" {
|
||||
continue // skip alpha/beta/rc
|
||||
}
|
||||
semverList = append(semverList, v)
|
||||
}
|
||||
|
||||
if len(semverList) == 0 {
|
||||
return "", fmt.Errorf("no stable Packer releases found in index at %s", indexURL)
|
||||
}
|
||||
|
||||
sort.Sort(semver.Collection(semverList))
|
||||
latest := semverList[len(semverList)-1]
|
||||
log.Printf("[INFO] Latest stable Packer version from releases index: %s", latest.Original())
|
||||
return latest.Original(), nil
|
||||
}
|
||||
|
||||
// downloadURLToTempFile downloads url into a new temp file and returns its path.
|
||||
// On any error the temp file is removed. The caller owns the returned file on success.
|
||||
func downloadURLToTempFile(ctx context.Context, client *http.Client, url, suffix string) (string, error) {
|
||||
f, err := os.CreateTemp("", "packer-dl-*"+suffix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
tmpPath := f.Name()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("HTTP request failed: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = f.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("HTTP %d for %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
_, copyErr := io.Copy(f, resp.Body)
|
||||
closeErr := f.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("failed to write download: %w", copyErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return "", fmt.Errorf("failed to close temp file: %w", closeErr)
|
||||
}
|
||||
|
||||
return tmpPath, nil
|
||||
}
|
||||
|
||||
// downloadChecksumFile fetches the SHA256SUMS text file at url.
|
||||
func downloadChecksumFile(ctx context.Context, client *http.Client, url string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build request for %s: %w", url, err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download %s: %w", url, err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download failed: HTTP %d for %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed reading response body for %s: %w", url, err)
|
||||
}
|
||||
if len(strings.TrimSpace(string(body))) == 0 {
|
||||
return "", fmt.Errorf("empty response body for %s", url)
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func isValidSHA256Hex(s string) bool {
|
||||
if len(s) != 64 {
|
||||
return false
|
||||
}
|
||||
_, err := hex.DecodeString(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func expectedZipSHA256FromSums(sumsContent, fileName string) (string, error) {
|
||||
for _, line := range strings.Split(sumsContent, "\n") {
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
candidateFileName := strings.TrimPrefix(fields[len(fields)-1], "*")
|
||||
if candidateFileName == fileName {
|
||||
hash := strings.ToLower(fields[0])
|
||||
if !isValidSHA256Hex(hash) {
|
||||
return "", fmt.Errorf("invalid SHA256 checksum format for %s in SHA256SUMS", fileName)
|
||||
}
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("checksum for %s not found in SHA256SUMS", fileName)
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open %s for hashing: %w", path, err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", fmt.Errorf("failed hashing %s: %w", path, err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// downloadPackerRelease fetches the latest stable Packer version from the
|
||||
// HashiCorp releases index (releases.hashicorp.com/packer/index.json), then
|
||||
// downloads and checksum-verifies the zip for the given GOOS/GOARCH.
|
||||
// All HTTP operations are retried up to three times.
|
||||
func downloadPackerRelease(ctx context.Context, goos, goarch string) (string, error) {
|
||||
base := getReleaseBaseURL()
|
||||
client := &http.Client{Timeout: 5 * time.Minute}
|
||||
|
||||
var zipPath string
|
||||
err := retry.Config{
|
||||
Tries: 3,
|
||||
RetryDelay: func() time.Duration { return 5 * time.Second },
|
||||
}.Run(ctx, func(ctx context.Context) error {
|
||||
// Resolve the latest stable version from the releases index.
|
||||
v, err := fetchLatestPackerVersion(ctx, client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine latest Packer version: %w", err)
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("packer_%s_%s_%s.zip", v, goos, goarch)
|
||||
zipURL := fmt.Sprintf("%s/packer/%s/%s", base, v, fileName)
|
||||
shaSumsURL := fmt.Sprintf("%s/packer/%s/packer_%s_SHA256SUMS", base, v, v)
|
||||
|
||||
log.Printf("[INFO] Downloading and verifying Packer %s for %s/%s...", v, goos, goarch)
|
||||
|
||||
candidateZipPath, err := downloadURLToTempFile(ctx, client, zipURL, ".zip")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download Packer release zip: %w", err)
|
||||
}
|
||||
keepCandidate := false
|
||||
defer func() {
|
||||
if !keepCandidate {
|
||||
_ = os.Remove(candidateZipPath)
|
||||
}
|
||||
}()
|
||||
|
||||
sumsContent, err := downloadChecksumFile(ctx, client, shaSumsURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download release checksums: %w", err)
|
||||
}
|
||||
|
||||
expectedSHA, err := expectedZipSHA256FromSums(sumsContent, fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve expected checksum: %w", err)
|
||||
}
|
||||
|
||||
actualSHA, err := fileSHA256(candidateZipPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.EqualFold(expectedSHA, actualSHA) {
|
||||
return fmt.Errorf("checksum mismatch for %s: expected %s, got %s", fileName, expectedSHA, actualSHA)
|
||||
}
|
||||
|
||||
// Validate the expected binary exists inside the archive.
|
||||
binaryName := "packer"
|
||||
if goos == "windows" {
|
||||
binaryName = "packer.exe"
|
||||
}
|
||||
|
||||
zr, err := zip.OpenReader(candidateZipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open downloaded zip: %w", err)
|
||||
}
|
||||
defer func() { _ = zr.Close() }()
|
||||
|
||||
foundBinary := false
|
||||
for _, f := range zr.File {
|
||||
if f.Name == binaryName {
|
||||
foundBinary = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBinary {
|
||||
return fmt.Errorf("packer binary %q not found in release zip %s", binaryName, zipURL)
|
||||
}
|
||||
|
||||
keepCandidate = true
|
||||
zipPath = candidateZipPath
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download and verify Packer release zip: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Downloaded and verified Packer release zip: %s", zipPath)
|
||||
return zipPath, nil
|
||||
}
|
||||
+328
-804
File diff suppressed because it is too large
Load Diff
@@ -22,9 +22,9 @@ type FlatConfig struct {
|
||||
Destination *string `mapstructure:"destination" required:"false" cty:"destination" hcl:"destination"`
|
||||
SbomName *string `mapstructure:"sbom_name" required:"false" cty:"sbom_name" hcl:"sbom_name"`
|
||||
AutoGenerate *bool `mapstructure:"auto_generate" required:"true" cty:"auto_generate" hcl:"auto_generate"`
|
||||
ScannerArgs []string `mapstructure:"scanner_args" required:"false" cty:"scanner_args" hcl:"scanner_args"`
|
||||
ScannerURL *string `mapstructure:"scanner_url" required:"false" cty:"scanner_url" hcl:"scanner_url"`
|
||||
ScannerChecksum *string `mapstructure:"scanner_checksum" required:"false" cty:"scanner_checksum" hcl:"scanner_checksum"`
|
||||
ScannerArgs []string `mapstructure:"scanner_args" required:"false" cty:"scanner_args" hcl:"scanner_args"`
|
||||
ScanPath *string `mapstructure:"scan_path" required:"false" cty:"scan_path" hcl:"scan_path"`
|
||||
ExecuteCommand *string `mapstructure:"execute_command" required:"false" cty:"execute_command" hcl:"execute_command"`
|
||||
ElevatedUser *string `mapstructure:"elevated_user" required:"false" cty:"elevated_user" hcl:"elevated_user"`
|
||||
@@ -55,9 +55,9 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||
"destination": &hcldec.AttrSpec{Name: "destination", Type: cty.String, Required: false},
|
||||
"sbom_name": &hcldec.AttrSpec{Name: "sbom_name", Type: cty.String, Required: false},
|
||||
"auto_generate": &hcldec.AttrSpec{Name: "auto_generate", Type: cty.Bool, Required: false},
|
||||
"scanner_args": &hcldec.AttrSpec{Name: "scanner_args", Type: cty.List(cty.String), Required: false},
|
||||
"scanner_url": &hcldec.AttrSpec{Name: "scanner_url", Type: cty.String, Required: false},
|
||||
"scanner_checksum": &hcldec.AttrSpec{Name: "scanner_checksum", Type: cty.String, Required: false},
|
||||
"scanner_args": &hcldec.AttrSpec{Name: "scanner_args", Type: cty.List(cty.String), Required: false},
|
||||
"scan_path": &hcldec.AttrSpec{Name: "scan_path", Type: cty.String, Required: false},
|
||||
"execute_command": &hcldec.AttrSpec{Name: "execute_command", Type: cty.String, Required: false},
|
||||
"elevated_user": &hcldec.AttrSpec{Name: "elevated_user", Type: cty.String, Required: false},
|
||||
|
||||
@@ -71,45 +71,24 @@ func TestConfigPrepare(t *testing.T) {
|
||||
&Config{
|
||||
AutoGenerate: true,
|
||||
ScanPath: "/",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json", "-q"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"auto_generate with custom scanner URL",
|
||||
"auto_generate with custom scan path",
|
||||
map[string]interface{}{
|
||||
"auto_generate": true,
|
||||
"scanner_url": "https://example.com/scanner",
|
||||
"scan_path": "/opt/app",
|
||||
},
|
||||
interpolate.Context{},
|
||||
&Config{
|
||||
AutoGenerate: true,
|
||||
ScannerURL: "https://example.com/scanner",
|
||||
ScanPath: "/opt/app",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json", "-q"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"auto_generate with scanner checksum and URL",
|
||||
map[string]interface{}{
|
||||
"auto_generate": true,
|
||||
"scanner_url": "https://example.com/scanner",
|
||||
"scanner_checksum": "abc123def456",
|
||||
},
|
||||
interpolate.Context{},
|
||||
&Config{
|
||||
AutoGenerate: true,
|
||||
ScannerURL: "https://example.com/scanner",
|
||||
ScannerChecksum: "abc123def456",
|
||||
ScanPath: "/",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json", "-q"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
false,
|
||||
"",
|
||||
@@ -124,12 +103,60 @@ func TestConfigPrepare(t *testing.T) {
|
||||
&Config{
|
||||
AutoGenerate: true,
|
||||
ScanPath: "/",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json", "-q"},
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json"},
|
||||
ExecuteCommand: "{{.Path}} {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"auto_generate with deprecated scanner_url (should warn but not fail)",
|
||||
map[string]interface{}{
|
||||
"auto_generate": true,
|
||||
"scanner_url": "https://example.com/scanner",
|
||||
"scan_path": "/opt/app",
|
||||
},
|
||||
interpolate.Context{},
|
||||
&Config{
|
||||
AutoGenerate: true,
|
||||
ScannerURL: "https://example.com/scanner",
|
||||
ScanPath: "/opt/app",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"deprecated scanner_checksum with scanner_url (should warn but not fail)",
|
||||
map[string]interface{}{
|
||||
"auto_generate": true,
|
||||
"scanner_url": "https://example.com/scanner",
|
||||
"scanner_checksum": "abc123def456",
|
||||
},
|
||||
interpolate.Context{},
|
||||
&Config{
|
||||
AutoGenerate: true,
|
||||
ScannerURL: "https://example.com/scanner",
|
||||
ScannerChecksum: "abc123def456",
|
||||
ScanPath: "/",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"deprecated scanner_checksum without scanner_url - should still error for clarity",
|
||||
map[string]interface{}{
|
||||
"auto_generate": true,
|
||||
"scanner_checksum": "abc123",
|
||||
},
|
||||
interpolate.Context{},
|
||||
nil,
|
||||
true,
|
||||
"scanner_checksum requires scanner_url",
|
||||
},
|
||||
{
|
||||
"auto_generate with elevated user and password",
|
||||
map[string]interface{}{
|
||||
@@ -143,8 +170,8 @@ func TestConfigPrepare(t *testing.T) {
|
||||
ElevatedUser: "admin",
|
||||
ElevatedPassword: "password123",
|
||||
ScanPath: "/",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json", "-q"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
ScannerArgs: []string{"-o", "cyclonedx-json"},
|
||||
ExecuteCommand: "chmod +x {{.Path}} && sudo {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
false,
|
||||
"",
|
||||
@@ -160,17 +187,6 @@ func TestConfigPrepare(t *testing.T) {
|
||||
true,
|
||||
"source and auto_generate are mutually exclusive",
|
||||
},
|
||||
{
|
||||
"scanner_checksum without scanner_url - should error",
|
||||
map[string]interface{}{
|
||||
"auto_generate": true,
|
||||
"scanner_checksum": "abc123",
|
||||
},
|
||||
interpolate.Context{},
|
||||
nil,
|
||||
true,
|
||||
"scanner_checksum requires scanner_url",
|
||||
},
|
||||
{
|
||||
"elevated_password without elevated_user - should error",
|
||||
map[string]interface{}{
|
||||
@@ -186,22 +202,18 @@ func TestConfigPrepare(t *testing.T) {
|
||||
"source mode with scanner fields - should succeed (allows toggling auto_generate)",
|
||||
map[string]interface{}{
|
||||
"source": "sbom.json",
|
||||
"scanner_url": "https://example.com/scanner",
|
||||
"scanner_checksum": "abc123",
|
||||
"scanner_args": []string{"-o", "json"},
|
||||
"scan_path": "/opt/app",
|
||||
"execute_command": "{{.Path}} {{.Args}}",
|
||||
"execute_command": "{{.Path}} sbom-generate {{.Args}}",
|
||||
"elevated_user": "admin",
|
||||
"elevated_password": "password123",
|
||||
},
|
||||
interpolate.Context{},
|
||||
&Config{
|
||||
Source: "sbom.json",
|
||||
ScannerURL: "https://example.com/scanner",
|
||||
ScannerChecksum: "abc123",
|
||||
ScannerArgs: []string{"-o", "json"},
|
||||
ScanPath: "/opt/app",
|
||||
ExecuteCommand: "{{.Path}} {{.Args}}",
|
||||
ExecuteCommand: "{{.Path}} sbom-generate {{.Args}}",
|
||||
ElevatedUser: "admin",
|
||||
ElevatedPassword: "password123",
|
||||
},
|
||||
@@ -238,3 +250,41 @@ func TestConfigPrepare(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeScannerExecuteCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "injects when path directly followed by args",
|
||||
in: "chmod +x {{.Path}} && {{.Path}} {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
want: "chmod +x {{.Path}} && {{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
{
|
||||
name: "injects when path directly followed by scan path",
|
||||
in: "sudo {{.Path}} {{.ScanPath}} > {{.Output}}",
|
||||
want: "sudo {{.Path}} sbom-generate {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
{
|
||||
name: "keeps command when sbom-generate already present",
|
||||
in: "{{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
want: "{{.Path}} sbom-generate {{.Args}} {{.ScanPath}} > {{.Output}}",
|
||||
},
|
||||
{
|
||||
name: "does not modify non-scan invocation",
|
||||
in: "chmod +x {{.Path}} && {{.Path}} version",
|
||||
want: "chmod +x {{.Path}} && {{.Path}} version",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeScannerExecuteCommand(tt.in)
|
||||
if got != tt.want {
|
||||
t.Fatalf("unexpected normalized command:\nwant: %q\n got: %q", tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
// Exclude platforms where containerd (a transitive dependency of syft) doesn't compile.
|
||||
// Containerd has platform-specific code that lacks support for NetBSD, OpenBSD, and Solaris.
|
||||
// This exclusion only affects dependency tracking; the hcp-sbom provisioner downloads
|
||||
// pre-built syft binaries at runtime and works on all platforms where those binaries exist.
|
||||
//go:build !netbsd && !openbsd && !solaris
|
||||
|
||||
package hcp_sbom
|
||||
|
||||
import (
|
||||
// Blank import to register Syft as a dependency
|
||||
// This file exists to declare Syft as a dependency for license and security scanning purposes.
|
||||
// While Packer downloads and executes Syft binaries at runtime, this import ensures
|
||||
// the Syft project appears in dependency analysis tools and SBOMs generated for Packer itself.
|
||||
_ "github.com/anchore/syft/syft"
|
||||
)
|
||||
@@ -17,6 +17,9 @@ page_title: hcp-sbom provisioner reference
|
||||
|
||||
The `hcp-sbom` provisioner uploads software bill of materials (SBOM) files from artifacts built by Packer to HCP Packer. You must format SBOM files you want to upload as JSON and follow either the [SPDX](https://spdx.github.io/spdx-spec/latest) or [CycloneDX](https://cyclonedx.org/) specification. HCP Packer ties these SBOM files to the version of the artifact that Packer builds.
|
||||
|
||||
## Deprecation Notice
|
||||
|
||||
~> **Deprecated Configuration Options:** The `scanner_url` and `scanner_checksum` configuration options are deprecated as of Packer v1.15.3 and will be removed in a future major version. The provisioner now uses a Packer binary with the embedded Syft SDK for the remote OS/arch for automatic SBOM generation; it downloads that binary from `releases.hashicorp.com` and uploads it to the target. For custom SBOM generation tools, use manual generation with the `source` field instead of `auto_generate`.
|
||||
## Example
|
||||
|
||||
The following example uploads an SBOM from the local `/tmp` directory and stores a copy at `./sbom/sbom_cyclonedx.json` on the local machine.
|
||||
@@ -47,6 +50,10 @@ provisioner "hcp-sbom" {
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Migrating from Custom Scanner URLs
|
||||
|
||||
Prior to v1.15.4, the `hcp-sbom` provisioner supported downloading custom scanner binaries via `scanner_url`. This is no longer supported as the provisioner now uses the Packer binary with embedded Syft SDK.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
You can specify the following configuration options.
|
||||
|
||||
Reference in New Issue
Block a user