feat: add SBOM generation command and integrate Syft SDK for SBOM creation

remove_syft_bin
Hari Om 2 weeks ago
parent 342fc1c554
commit cc75d71745

@ -0,0 +1,119 @@
package command
//Types to define SBOM generation command
// - Command struct (e.g., SBOMGenerateCommand)
// - Flags for the command (e.g., --format, --output)
// - Method to execute the command (e.g., Run)
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
}
//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
default:
// Assume it's the scan path (positional argument)
if !strings.HasPrefix(arg, "-") {
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>
Generate a Software Bill of Materials (SBOM) for the local filesystem.
This command is typically invoked internally by the hcp-sbom provisioner.
Options:
-o <format> Output format: cyclonedx-json, spdx-json (default: cyclonedx-json)
Arguments:
<path> Path to scan (default: /)
Examples:
# Generate CycloneDX SBOM for root filesystem
packer sbom-generate -o cyclonedx-json / > sbom.json
# Generate SPDX SBOM
packer sbom-generate -o spdx-json / > sbom.json
# Scan specific directory
packer sbom-generate -o cyclonedx-json /opt/app > app-sbom.json
Note: Output is written to stdout. Use shell redirection (>) to save to file.
`
return strings.TrimSpace(helpText)
}

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

@ -61,8 +61,10 @@ require (
github.com/oklog/ulid v1.3.1
github.com/pierrec/lz4/v4 v4.1.22
github.com/shirou/gopsutil/v3 v3.23.4
github.com/sirupsen/logrus v1.9.4
github.com/spdx/tools-golang v0.5.7
google.golang.org/grpc v1.79.3
modernc.org/sqlite v1.46.1
)
require (
@ -303,6 +305,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 +328,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
@ -335,7 +339,6 @@ require (
github.com/sergi/go-diff v1.4.0 // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // 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.9

@ -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,57 @@
package sbom
import (
"fmt"
"strings"
)
// Format represents the SBOM output format
type Format string
const (
FormatCycloneDX Format = "cyclonedx"
FormatSPDX Format = "spdx"
)
// 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)
}
// 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
}
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)
}

@ -0,0 +1,77 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
//go:build !netbsd && !openbsd && !solaris
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
src, err := syft.GetSource(ctx, sourceInput, nil)
if err != nil {
return nil, fmt.Errorf("failed to get source: %w", err)
}
defer src.Close()
sbomCfg := syft.DefaultCreateSBOMConfig().
WithSearchConfig(cataloging.SearchConfig{
Scope: source.SquashedScope,
}).
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:
encoder, err := cyclonedxjson.NewFormatEncoderWithConfig(
cyclonedxjson.EncoderConfig{
Version: "1.5",
Pretty: true,
},
)
if err != nil {
return nil, fmt.Errorf("failed to create CycloneDX encoder: %w", err)
}
return format.Encode(*sbomData, encoder)
case FormatSPDX:
encoder, err := spdxjson.NewFormatEncoderWithConfig(
spdxjson.EncoderConfig{
Version: "2.3",
Pretty: true,
},
)
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
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,

@ -1 +1 @@
1.15.3
1.15.3-dev

Loading…
Cancel
Save