Skip to content

Commit

Permalink
fix: inspecting sboms on simple pkgs (#924)
Browse files Browse the repository at this point in the history
  • Loading branch information
UncleGedd authored Sep 19, 2024
1 parent 405e430 commit 1353dfd
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 72 deletions.
10 changes: 9 additions & 1 deletion src/pkg/bundle/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
func (b *Bundle) Inspect() error {
// print to stdout to enable users to easily grab the output
pterm.SetDefaultOutput(os.Stdout)
var warns []string

if err := utils.CheckYAMLSourcePath(b.cfg.InspectOpts.Source); err == nil {
b.cfg.InspectOpts.IsYAMLFile = true
Expand Down Expand Up @@ -71,7 +72,7 @@ func (b *Bundle) Inspect() error {

// pull sbom
if b.cfg.InspectOpts.IncludeSBOM {
err := provider.CreateBundleSBOM(b.cfg.InspectOpts.ExtractSBOM, b.bundle.Metadata.Name)
warns, err = provider.CreateBundleSBOM(b.cfg.InspectOpts.ExtractSBOM, b.bundle.Metadata.Name)
if err != nil {
return err
}
Expand All @@ -97,6 +98,13 @@ func (b *Bundle) Inspect() error {
}

zarfUtils.ColorPrintYAML(b.bundle, nil, false)

// print warnings to stderr
pterm.SetDefaultOutput(os.Stderr)
for _, warn := range warns {
message.Warnf(warn)
}

return nil
}

Expand Down
2 changes: 1 addition & 1 deletion src/pkg/bundle/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Provider interface {
LoadBundle(options types.BundlePullOptions, concurrency int) (*types.UDSBundle, types.PathMap, error)

// CreateBundleSBOM creates a bundle-level SBOM from the underlying Zarf packages, if the Zarf package contains an SBOM
CreateBundleSBOM(extractSBOM bool, bundleName string) error
CreateBundleSBOM(extractSBOM bool, bundleName string) ([]string, error)

// PublishBundle publishes a bundle to a remote OCI repo
PublishBundle(bundle types.UDSBundle, remote *oci.OrasRemote) error
Expand Down
38 changes: 10 additions & 28 deletions src/pkg/bundle/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,20 @@ func (op *ociProvider) LoadBundleMetadata() (types.PathMap, error) {
}

// CreateBundleSBOM creates a bundle-level SBOM from the underlying Zarf packages, if the Zarf package contains an SBOM
func (op *ociProvider) CreateBundleSBOM(extractSBOM bool, bundleName string) error {
func (op *ociProvider) CreateBundleSBOM(extractSBOM bool, bundleName string) ([]string, error) {
var warns []string
ctx := context.TODO()
SBOMArtifactPathMap := make(types.PathMap)
root, err := op.FetchRoot(ctx)
if err != nil {
return err
return warns, err
}

// make tmp dir for pkg SBOM extraction
err = os.Mkdir(filepath.Join(op.dst, config.BundleSBOM), 0700)
if err != nil {
return err
return warns, err
}
containsSBOMs := false

// iterate through Zarf image manifests and find the Zarf pkg's sboms.tar
for _, layer := range root.Layers {
Expand All @@ -107,7 +107,7 @@ func (op *ociProvider) CreateBundleSBOM(extractSBOM bool, bundleName string) err
}
zarfManifest, err := op.OrasRemote.FetchManifest(ctx, layer)
if err != nil {
return err
return warns, err
}
// grab descriptor for sboms.tar
sbomDesc := zarfManifest.Locate(config.SBOMsTar)
Expand All @@ -118,35 +118,17 @@ func (op *ociProvider) CreateBundleSBOM(extractSBOM bool, bundleName string) err
// grab sboms.tar and extract
sbomBytes, err := op.OrasRemote.FetchLayer(ctx, sbomDesc)
if err != nil {
return err
return warns, err
}

extractor := utils.SBOMExtractor(op.dst, SBOMArtifactPathMap)
err = archiver.Tar{}.Extract(context.TODO(), bytes.NewReader(sbomBytes), nil, extractor)
if err != nil {
return err
return warns, err
}
containsSBOMs = true
}
if extractSBOM {
if !containsSBOMs {
message.Warnf("Cannot extract, no SBOMs found in bundle")
return nil
}
currentDir, err := os.Getwd()
if err != nil {
return err
}
err = utils.MoveExtractedSBOMs(bundleName, op.dst, currentDir)
if err != nil {
return err
}
} else {
err = utils.CreateSBOMArtifact(SBOMArtifactPathMap, bundleName)
if err != nil {
return err
}
}
return nil

return utils.HandleSBOM(extractSBOM, SBOMArtifactPathMap, bundleName, op.dst)
}

// LoadBundle loads a bundle from a remote source
Expand Down
58 changes: 26 additions & 32 deletions src/pkg/bundle/tarball.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,20 @@ type tarballBundleProvider struct {
}

// CreateBundleSBOM creates a bundle-level SBOM from the underlying Zarf packages, if the Zarf package contains an SBOM
func (tp *tarballBundleProvider) CreateBundleSBOM(extractSBOM bool, bundleName string) error {
func (tp *tarballBundleProvider) CreateBundleSBOM(extractSBOM bool, bundleName string) ([]string, error) {
var warns []string
rootManifest, err := tp.getBundleManifest()
if err != nil {
return err
return warns, err
}
// make tmp dir for pkg SBOM extraction
err = os.Mkdir(filepath.Join(tp.dst, config.BundleSBOM), 0o700)
if err != nil {
return err
return warns, err
}

// track SBOM artifact paths, used for extraction and creation of bundleSBOM artifact
SBOMArtifactPathMap := make(types.PathMap)
containsSBOMs := false

for _, layer := range rootManifest.Layers {
// get Zarf image manifests from bundle manifest
Expand All @@ -58,63 +60,55 @@ func (tp *tarballBundleProvider) CreateBundleSBOM(extractSBOM bool, bundleName s
}
layerFilePath := filepath.Join(config.BlobsDir, layer.Digest.Encoded())
if err := av3.Extract(tp.src, layerFilePath, tp.dst); err != nil {
return fmt.Errorf("failed to extract %s from %s: %w", layer.Digest.Encoded(), tp.src, err)
return warns, fmt.Errorf("failed to extract %s from %s: %w", layer.Digest.Encoded(), tp.src, err)
}

// read in and unmarshal Zarf image manifest
zarfManifestBytes, err := os.ReadFile(filepath.Join(tp.dst, layerFilePath))
if err != nil {
return err
return warns, err
}
var zarfImageManifest *oci.Manifest
if err := json.Unmarshal(zarfManifestBytes, &zarfImageManifest); err != nil {
return err
return warns, err
}

// find sbom layer descriptor and extract sbom tar from archive
sbomDesc := zarfImageManifest.Locate(config.SBOMsTar)

// if sbomDesc doesn't exist, continue
if oci.IsEmptyDescriptor(sbomDesc) {
message.Warnf("%s not found in Zarf pkg", config.SBOMsTar)
continue
}

sbomFilePath := filepath.Join(config.BlobsDir, sbomDesc.Digest.Encoded())

// check if file path already exists and remove
// this fixes a bug where multiple pkgs have an empty SBOM tar archive
if _, err := os.Stat(filepath.Join(tp.dst, sbomFilePath)); err == nil {
err = os.Remove(filepath.Join(tp.dst, sbomFilePath))
if err != nil {
return warns, err
}
}

if err := av3.Extract(tp.src, sbomFilePath, tp.dst); err != nil {
return fmt.Errorf("failed to extract %s from %s: %w", layer.Digest.Encoded(), tp.src, err)
return warns, fmt.Errorf("failed to extract %s from %s: %w", layer.Digest.Encoded(), tp.src, err)
}
sbomTarBytes, err := os.ReadFile(filepath.Join(tp.dst, sbomFilePath))
if err != nil {
return err
return warns, err
}
extractor := utils.SBOMExtractor(tp.dst, SBOMArtifactPathMap)

// extract SBOMs from tar
err = av4.Tar{}.Extract(context.TODO(), bytes.NewReader(sbomTarBytes), nil, extractor)
if err != nil {
return err
return warns, err
}
containsSBOMs = true
}
if extractSBOM {
if !containsSBOMs {
message.Warnf("Cannot extract, no SBOMs found in bundle")
return nil
}
currentDir, err := os.Getwd()
if err != nil {
return err
}
err = utils.MoveExtractedSBOMs(bundleName, tp.dst, currentDir)
if err != nil {
return err
}
} else {
err = utils.CreateSBOMArtifact(SBOMArtifactPathMap, bundleName)
if err != nil {
return err
}
}
return nil

return utils.HandleSBOM(extractSBOM, SBOMArtifactPathMap, bundleName, tp.dst)
}

func (tp *tarballBundleProvider) getBundleManifest() (*oci.Manifest, error) {
Expand Down
37 changes: 33 additions & 4 deletions src/pkg/utils/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
"github.com/mholt/archiver/v4"
)

// CreateSBOMArtifact creates sbom artifacts in the form of a tar archive
func CreateSBOMArtifact(SBOMArtifactPathMap map[string]string, bundleName string) error {
// createSBOMArtifact creates sbom artifacts in the form of a tar archive
func createSBOMArtifact(SBOMArtifactPathMap map[string]string, bundleName string) error {
out, err := os.Create(fmt.Sprintf("%s-%s", bundleName, config.BundleSBOMTar))
if err != nil {
return err
Expand All @@ -33,8 +33,8 @@ func CreateSBOMArtifact(SBOMArtifactPathMap map[string]string, bundleName string
return nil
}

// MoveExtractedSBOMs moves the extracted SBOM HTML and JSON files from src to dst
func MoveExtractedSBOMs(bundleName, src, dst string) error {
// moveExtractedSBOMs moves the extracted SBOM HTML and JSON files from src to dst
func moveExtractedSBOMs(bundleName, src, dst string) error {
srcSBOMPath := filepath.Join(src, config.BundleSBOM)
extractDirName := fmt.Sprintf("%s-%s", bundleName, config.BundleSBOM)
sbomDir := filepath.Join(dst, extractDirName)
Expand Down Expand Up @@ -86,3 +86,32 @@ func SBOMExtractor(dst string, SBOMArtifactPathMap map[string]string) func(_ con
}
return extractor
}

// HandleSBOM handles the extraction and creation of bundle SBOMs after populating SBOMArtifactPathMap
func HandleSBOM(extractSBOM bool, SBOMArtifactPathMap map[string]string, bundleName, dstPath string) ([]string, error) {
var warns []string

if extractSBOM {
if len(SBOMArtifactPathMap) == 0 {
warns = append(warns, "Cannot extract, no SBOMs found in bundle")
return warns, nil
}
currentDir, err := os.Getwd()
if err != nil {
return warns, err
}
err = moveExtractedSBOMs(bundleName, dstPath, currentDir)
if err != nil {
return warns, err
}
} else if len(SBOMArtifactPathMap) > 0 {
err := createSBOMArtifact(SBOMArtifactPathMap, bundleName)
if err != nil {
return warns, err
}
} else {
warns = append(warns, "No SBOMs found in bundle")
}

return warns, nil
}
15 changes: 15 additions & 0 deletions src/test/bundles/11-real-simple/multiple-simple/uds-bundle.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
kind: UDSBundle
metadata:
name: multiple-simple
description: |
bundles multiple simple pkgs to test SBOM generation during inspect,
note that no sboms are in the bundled pkgs
version: 0.0.1

packages:
- name: real-simple
path: "../../../packages/no-cluster/real-simple"
ref: 0.0.1
- name: output-var
path: "../../../packages/no-cluster/output-var"
ref: 0.0.1
41 changes: 35 additions & 6 deletions src/test/e2e/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,37 @@ func TestLocalBundleWithOutput(t *testing.T) {
runCmd(t, fmt.Sprintf("inspect %s", bundlePath))
}

func TestSimplePackagesWithSBOMs(t *testing.T) {
// tests that this bug is resolved: https://github.com/defenseunicorns/uds-cli/issues/923
e2e.CreateZarfPkg(t, "src/test/packages/no-cluster/output-var", false)
e2e.CreateZarfPkg(t, "src/test/packages/no-cluster/real-simple", false)

bundleDir := "src/test/bundles/11-real-simple/multiple-simple"
bundlePath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-multiple-simple-%s-0.0.1.tar.zst", e2e.Arch))
runCmd(t, fmt.Sprintf("create %s --confirm -a %s", bundleDir, e2e.Arch))

t.Run("test local bundle with simple packages and no SBOMs", func(t *testing.T) {
_, stderr := runCmd(t, fmt.Sprintf("inspect %s --sbom", bundlePath))
require.Contains(t, stderr, "No SBOMs found in bundle")
_, stderr = runCmd(t, fmt.Sprintf("inspect %s --sbom --extract", bundlePath))
require.Contains(t, stderr, "Cannot extract, no SBOMs found in bundle")
})

t.Run("test remote bundle with simple packages and no SBOMs", func(t *testing.T) {
// publish bundle to registry
e2e.SetupDockerRegistry(t, 888)
defer e2e.TeardownRegistry(t, 888)
runCmd(t, fmt.Sprintf("publish %s %s --insecure", bundlePath, "localhost:888"))

// inspect bundle for sboms
remoteBundlePath := "localhost:888/multiple-simple:0.0.1"
_, stderr := runCmd(t, fmt.Sprintf("inspect %s --insecure --sbom", remoteBundlePath))
require.Contains(t, stderr, "No SBOMs found in bundle")
_, stderr = runCmd(t, fmt.Sprintf("inspect %s --insecure --sbom --extract", remoteBundlePath))
require.Contains(t, stderr, "Cannot extract, no SBOMs found in bundle")
})
}

func TestLocalBundleWithNoSBOM(t *testing.T) {
path := "src/test/packages/nginx"
runCmd(t, fmt.Sprintf("zarf package create %s -o %s --skip-sbom --confirm", path, path))
Expand All @@ -249,9 +280,8 @@ func TestLocalBundleWithNoSBOM(t *testing.T) {
bundlePath := filepath.Join(bundleDir, fmt.Sprintf("uds-bundle-yml-example-%s-0.0.1.tar.zst", e2e.Arch))
runCmd(t, fmt.Sprintf("create %s --insecure --confirm -a %s", bundleDir, e2e.Arch))

stdout, _ := runCmd(t, fmt.Sprintf("inspect %s --sbom --extract", bundlePath))
require.Contains(t, stdout, "Cannot extract, no SBOMs found in bundle")
require.Contains(t, stdout, "sboms.tar not found in Zarf pkg")
_, stderr := runCmd(t, fmt.Sprintf("inspect %s --sbom --extract", bundlePath))
require.Contains(t, stderr, "Cannot extract, no SBOMs found in bundle")
}

func TestRemoteBundleWithNoSBOM(t *testing.T) {
Expand All @@ -267,9 +297,8 @@ func TestRemoteBundleWithNoSBOM(t *testing.T) {
runCmd(t, fmt.Sprintf("create %s --insecure --confirm -a %s", bundleDir, e2e.Arch))
runCmd(t, fmt.Sprintf("publish %s %s --insecure", bundlePath, "localhost:888"))

stdout, _ := runCmd(t, fmt.Sprintf("inspect %s --sbom --extract", bundlePath))
require.Contains(t, stdout, "Cannot extract, no SBOMs found in bundle")
require.Contains(t, stdout, "sboms.tar not found in Zarf pkg")
_, stderr := runCmd(t, fmt.Sprintf("inspect %s --sbom --extract", bundlePath))
require.Contains(t, stderr, "Cannot extract, no SBOMs found in bundle")
}

func TestPackageNaming(t *testing.T) {
Expand Down

0 comments on commit 1353dfd

Please sign in to comment.