feat: add support for --exclude and --scope flags in SBOM generation command

Co-authored-by: Copilot <copilot@github.com>
remove_syft_bin
Hari Om 1 week ago
parent 31af13a612
commit fc5d02fdc1

@ -34,6 +34,7 @@ func (cmd *SBOMGenerateCommand) ParseArgs(args []string) (*sbom.Config, int) {
ScanPath: "/",
Format: sbom.FormatCycloneDX, // default format
Parallelism: 4, // default parallelism
Scope: sbom.ScopeSquashed, // default scope
}
//Parse Syft Style args
@ -59,11 +60,60 @@ func (cmd *SBOMGenerateCommand) ParseArgs(args []string) (*sbom.Config, int) {
}
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:
// Assume it's the scan path (positional argument)
if !strings.HasPrefix(arg, "-") {
cfg.ScanPath = arg
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, "-") {
cmd.Ui.Say(fmt.Sprintf("Warning: unsupported sbom-generate argument ignored: %s", arg))
continue
}
// Assume it's the scan path (positional argument)
cfg.ScanPath = arg
}
}
return cfg, 0
@ -104,6 +154,8 @@ Usage: packer sbom-generate [options] <path>
This command is typically invoked internally 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: /)
Examples:
@ -113,6 +165,8 @@ Examples:
packer sbom-generate -o spdx-json / > sbom.json
# Scan specific directory
packer sbom-generate -o cyclonedx-json /opt/app > app-sbom.json
# Scan all image layers and exclude temporary paths
packer sbom-generate --scope all-layers --exclude "/tmp/**" -o cyclonedx-json / > sbom.json
Note: Output is written to stdout. Use shell redirection (>) to save to file.
`
return strings.TrimSpace(helpText)

@ -0,0 +1,89 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"bytes"
"strings"
"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_UnsupportedFlagWarns(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 !strings.Contains(out.String(), "unsupported sbom-generate argument ignored: -q") {
t.Fatalf("expected warning for unsupported arg, got output %q", out.String())
}
}

@ -11,6 +11,8 @@ type Format string
const (
FormatCycloneDX Format = "cyclonedx"
FormatSPDX Format = "spdx"
ScopeSquashed string = "squashed"
ScopeAllLayers string = "all-layers"
)
// Config holds configuration for SBOM generation
@ -18,6 +20,8 @@ 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
@ -37,6 +41,9 @@ func NewGenerator(cfg Config) *Generator {
if cfg.Parallelism == 0 {
cfg.Parallelism = 4
}
if cfg.Scope == "" {
cfg.Scope = ScopeSquashed
}
return &Generator{config: cfg}
}
@ -55,3 +62,16 @@ func ParseFormatFromArgs(formatArg string) (Format, error) {
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)
}
}

@ -22,17 +22,31 @@ import (
// 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)
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 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: source.SquashedScope,
Scope: scope,
}).
WithParallelism(g.config.Parallelism)

Loading…
Cancel
Save