Repository: chainguard-dev/apko
Pull request #94 (2022-03-11 06:17:21)

SBOM Support!
This PR finally exposes the SBOM functionality in `apko build` and completes some missing bits in the SBOM code.

It introduces three new flags to control SBOM generation:

```
      --sbom                   generate SBOMs (default true)
      --sbom-formats strings   SBOM formats to output (default [spdx,cyclonedx])
      --sbom-path string       generate SBOMs in dir (defaults to image directory)
```
All of these flags are optional. The default behaviour is to write the pair of sboms in both formats in the same directory of the image tarball. 

There are other changes in the PR geared towards completing the information in the SBOMs:

1. SBOM generation has been moved after the layer is written as the digest is needed to generate purls
2. Bumped the alpine/go module to add the missing package information to the sboms
3.   Added remaining information like architecture, layer digest, and other data

I think we need to do some adjustments to the SBOM structure because we are not describing the image, only the layer. But let's handle this in a follow-up. 



Patch:
diff --git a/go.mod b/go.mod
index efd3cc7..348c60d 100644
--- a/go.mod
+++ b/go.mod
@@ -11,9 +11,10 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.4.1
+	github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a
 	github.com/spf13/cobra v1.3.0
 	github.com/stretchr/testify v1.7.0
-	gitlab.alpinelinux.org/alpine/go v0.3.0
+	gitlab.alpinelinux.org/alpine/go v0.3.1
 	go.lsp.dev/uri v0.3.0
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
diff --git a/go.sum b/go.sum
index b214f90..0e15f82 100644
--- a/go.sum
+++ b/go.sum
@@ -901,6 +901,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
 github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
 github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
 github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
+github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a h1:tkTSd1nhioPqi5Whu3CQ79UjPtaGOytqyNnSCVOqzHM=
+github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
@@ -1124,8 +1126,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
 github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
 github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
-gitlab.alpinelinux.org/alpine/go v0.3.0 h1:4wVjXZRAd4rApnvVEFZqReDTdOe8ZLEKa8/egMPqVJM=
-gitlab.alpinelinux.org/alpine/go v0.3.0/go.mod h1:auOw3SnxDQBo1vzPh8q6gjvKsYgKxYet03lgSKtA3Q4=
+gitlab.alpinelinux.org/alpine/go v0.3.1 h1:mZEtxIt4YAS2xqbtfQL7YqCvzbjV/8tRHasuHSC2Vho=
+gitlab.alpinelinux.org/alpine/go v0.3.1/go.mod h1:auOw3SnxDQBo1vzPh8q6gjvKsYgKxYet03lgSKtA3Q4=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
diff --git a/pkg/build/build.go b/pkg/build/build.go
index 0e6074b..c4ddc16 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -35,6 +35,7 @@ type Context struct {
 	Assertions         []Assertion
 	WantSBOM           bool
 	SBOMPath           string
+	SBOMFormats        []string
 	Arch               types.Architecture
 }
 
@@ -61,6 +62,7 @@ func (bc *Context) BuildTarball() (string, error) {
 	if err != nil {
 		return "", fmt.Errorf("opening the build context tarball path failed: %w", err)
 	}
+	bc.TarballPath = outfile.Name()
 	defer outfile.Close()
 
 	tw, err := tarball.NewContext(tarball.WithSourceDateEpoch(bc.SourceDateEpoch))
@@ -90,6 +92,11 @@ func (bc *Context) BuildLayer() (string, error) {
 		return "", err
 	}
 
+	// generate SBOM
+	if err := bc.GenerateSBOM(); err != nil {
+		return "", fmt.Errorf("generating SBOMs: %w", err)
+	}
+
 	return layerTarGZ, nil
 }
 
@@ -209,6 +216,13 @@ func WithSBOM(path string) Option {
 	}
 }
 
+func WithSBOMFormats(formats []string) Option {
+	return func(bc *Context) error {
+		bc.SBOMFormats = formats
+		return nil
+	}
+}
+
 // WithImageConfiguration sets the ImageConfiguration object
 // to use when building.
 func WithImageConfiguration(ic types.ImageConfiguration) Option {
diff --git a/pkg/build/image_builder.go b/pkg/build/image_builder.go
index 69a6088..ff6e5f0 100644
--- a/pkg/build/image_builder.go
+++ b/pkg/build/image_builder.go
@@ -105,13 +105,6 @@ func (bc *Context) BuildImage() error {
 		return fmt.Errorf("failed to write supervision tree: %w", err)
 	}
 
-	// generate SBOM
-	if bc.SBOMPath != "" {
-		if err := bc.GenerateSBOM(); err != nil {
-			return fmt.Errorf("failed to generate SBOM: %w", err)
-		}
-	}
-
 	log.Printf("finished building filesystem in %s", bc.WorkDir)
 	return nil
 }
diff --git a/pkg/build/sbom.go b/pkg/build/sbom.go
index 3b40e6a..7a1b3c9 100644
--- a/pkg/build/sbom.go
+++ b/pkg/build/sbom.go
@@ -19,23 +19,52 @@ import (
 	"log"
 
 	"chainguard.dev/apko/pkg/sbom"
+	"github.com/google/go-containerregistry/pkg/name"
+	v1tar "github.com/google/go-containerregistry/pkg/v1/tarball"
 )
 
 // GenerateSBOM runs the sbom generation
 func (bc *Context) GenerateSBOM() error {
+	if len(bc.SBOMFormats) == 0 {
+		log.Printf("skipping SBOM generation")
+		return nil
+	}
 	log.Printf("generating SBOM")
 
 	// TODO(puerco): Split GenerateSBOM into context implementation
 	s := sbom.NewWithWorkDir(bc.WorkDir)
 
+	v1Layer, err := v1tar.LayerFromFile(bc.TarballPath)
+	if err != nil {
+		return fmt.Errorf("failed to create OCI layer from tar.gz: %w", err)
+	}
+
+	digest, err := v1Layer.Digest()
+	if err != nil {
+		return fmt.Errorf("could not calculate layer digest: %w", err)
+	}
+
+	// Parse the image reference
+	if len(bc.Tags) > 0 {
+		tag, err := name.NewTag(bc.Tags[0])
+		if err != nil {
+			return fmt.Errorf("parsing tag %s: %w", bc.Tags[0], err)
+		}
+		s.Options.ImageInfo.Tag = tag.TagStr()
+		s.Options.ImageInfo.Name = tag.String()
+	}
+
 	// Generate the packages externally as we may
 	// move the package reader somewhere else
 	packages, err := s.ReadPackageIndex()
 	if err != nil {
-		return fmt.Errorf("getting installed packagesx from sbom: %w", err)
+		return fmt.Errorf("getting installed packages from sbom: %w", err)
 	}
+	s.Options.ImageInfo.Arch = bc.Arch
+	s.Options.ImageInfo.Digest = digest.String()
 	s.Options.OutputDir = bc.SBOMPath
 	s.Options.Packages = packages
+	s.Options.Formats = bc.SBOMFormats
 
 	if _, err := s.Generate(); err != nil {
 		return fmt.Errorf("generating SBOMs: %w", err)
diff --git a/pkg/cli/build.go b/pkg/cli/build.go
index d92a0ce..a05fa40 100644
--- a/pkg/cli/build.go
+++ b/pkg/cli/build.go
@@ -19,18 +19,22 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"path/filepath"
 	"runtime"
 
 	"chainguard.dev/apko/pkg/build"
 	"chainguard.dev/apko/pkg/build/oci"
 	"chainguard.dev/apko/pkg/build/types"
+	"chainguard.dev/apko/pkg/sbom"
 	"github.com/spf13/cobra"
 )
 
 func Build() *cobra.Command {
 	var useProot bool
 	var buildDate string
+	var writeSBOM bool
 	var sbomPath string
+	var sbomFormats []string
 
 	cmd := &cobra.Command{
 		Use:   "build",
@@ -40,28 +44,39 @@ func Build() *cobra.Command {
 The generated image is in a format which can be used with the "docker load"
 command, e.g.
 
-  # docker load < output.tar`,
+  # docker load < output.tar
+
+Along the image, apko will generate CycloneDX and SPDX SBOMs (software 
+bill of materials) describing the image contents.
+`,
 		Example: `  apko build <config.yaml> <tag> <output.tar>`,
 		Args:    cobra.ExactArgs(3),
 		RunE: func(cmd *cobra.Command, args []string) error {
+			if !writeSBOM {
+				sbomFormats = []string{}
+			}
 			return BuildCmd(cmd.Context(), args[1], args[2],
 				build.WithConfig(args[0]),
 				build.WithProot(useProot),
 				build.WithBuildDate(buildDate),
 				build.WithAssertions(build.RequireGroupFile(true), build.RequirePasswdFile(true)),
 				build.WithSBOM(sbomPath),
+				build.WithSBOMFormats(sbomFormats),
+				build.WithTags(args[1]),
 			)
 		},
 	}
 
 	cmd.Flags().BoolVar(&useProot, "use-proot", false, "use proot to simulate privileged operations")
 	cmd.Flags().StringVar(&buildDate, "build-date", "", "date used for the timestamps of the files inside the image")
-	cmd.Flags().StringVar(&sbomPath, "sbom-path", "", "generate an SBOM")
+	cmd.Flags().BoolVar(&writeSBOM, "sbom", true, "generate SBOMs")
+	cmd.Flags().StringVar(&sbomPath, "sbom-path", "", "generate SBOMs in dir (defaults to image directory)")
+	cmd.Flags().StringSliceVar(&sbomFormats, "sbom-formats", sbom.DefaultOptions.Formats, "SBOM formats to output")
 
 	return cmd
 }
 
-func BuildCmd(ctx context.Context, imageRef string, outputTarGZ string, opts ...build.Option) error {
+func BuildCmd(ctx context.Context, imageRef, outputTarGZ string, opts ...build.Option) error {
 	wd, err := os.MkdirTemp("", "apko-*")
 	if err != nil {
 		return fmt.Errorf("failed to create working directory: %w", err)
@@ -72,6 +87,15 @@ func BuildCmd(ctx context.Context, imageRef string, outputTarGZ string, opts ...
 	if err != nil {
 		return err
 	}
+
+	if bc.SBOMPath == "" {
+		dir, err := filepath.Abs(outputTarGZ)
+		if err != nil {
+			return fmt.Errorf("resolving output file path: %w", err)
+		}
+		bc.SBOMPath = filepath.Dir(dir)
+	}
+
 	arch := types.Architecture(runtime.GOARCH)
 	if len(bc.ImageConfiguration.Archs) != 0 {
 		log.Printf("WARNING: ignoring archs in config, only building for current arch (%s)", arch)
@@ -86,7 +110,13 @@ func BuildCmd(ctx context.Context, imageRef string, outputTarGZ string, opts ...
 	}
 	defer os.Remove(layerTarGZ)
 
-	if err := oci.BuildImageTarballFromLayer(imageRef, layerTarGZ, outputTarGZ, bc.ImageConfiguration, bc.SourceDateEpoch, arch); err != nil {
+	if err := bc.GenerateSBOM(); err != nil {
+		return fmt.Errorf("generating SBOMs: %w", err)
+	}
+
+	if err := oci.BuildImageTarballFromLayer(
+		imageRef, layerTarGZ, outputTarGZ, bc.ImageConfiguration, bc.SourceDateEpoch, arch,
+	); err != nil {
 		return fmt.Errorf("failed to build OCI image: %w", err)
 	}
 
diff --git a/pkg/sbom/generator/cyclonedx/cyclonedx.go b/pkg/sbom/generator/cyclonedx/cyclonedx.go
index 0ce52f6..4379691 100644
--- a/pkg/sbom/generator/cyclonedx/cyclonedx.go
+++ b/pkg/sbom/generator/cyclonedx/cyclonedx.go
@@ -21,7 +21,7 @@ import (
 	"strings"
 
 	"chainguard.dev/apko/pkg/sbom/options"
-	"chainguard.dev/apko/pkg/sbom/purl"
+	purl "github.com/package-url/packageurl-go"
 )
 
 type CycloneDX struct{}
@@ -43,10 +43,14 @@ func (cdx *CycloneDX) Generate(opts *options.Options, path string) error {
 	pkgComponents := []Component{}
 	pkgDependencies := []Dependency{}
 
+	mm := map[string]string{"arch": opts.ImageInfo.Arch.ToAPK()}
+
 	for _, pkg := range opts.Packages {
 		// add the component
 		c := Component{
-			BOMRef:      purl.Package(opts.OS.ID, pkg),
+			BOMRef: purl.NewPackageURL(
+				"apk", opts.OS.ID, pkg.Name, pkg.Version,
+				purl.QualifiersFromMap(mm), "").String(),
 			Name:        pkg.Name,
 			Version:     pkg.Version,
 			Description: pkg.Description,
@@ -55,7 +59,9 @@ func (cdx *CycloneDX) Generate(opts *options.Options, path string) error {
 					Expression: pkg.License,
 				},
 			},
-			PUrl: purl.Package(opts.OS.ID, pkg),
+			PUrl: purl.NewPackageURL(
+				"apk", opts.OS.ID, pkg.Name, pkg.Version,
+				purl.QualifiersFromMap(mm), "").String(),
 			// TODO(kaniini): Talk with CycloneDX people about adding "package" type.
 			Type: "operating-system",
 		}
@@ -78,18 +84,35 @@ func (cdx *CycloneDX) Generate(opts *options.Options, path string) error {
 				continue
 			}
 
-			depRefs = append(depRefs, fmt.Sprintf("pkg:apk/%s/%s", opts.OS.ID, dep))
+			depRefs = append(depRefs, purl.NewPackageURL("apk", opts.OS.ID, pkg.Name, "",
+				purl.QualifiersFromMap(mm), "").String())
 		}
 
 		d := Dependency{
-			Ref:       purl.Package(opts.OS.ID, pkg),
+			Ref: purl.NewPackageURL(
+				"apk", opts.OS.ID, pkg.Name, pkg.Version,
+				purl.QualifiersFromMap(mm), "").String(),
 			DependsOn: depRefs,
 		}
 		pkgDependencies = append(pkgDependencies, d)
 	}
 
+	// Main package purl qualifiers
+	mmMain := map[string]string{}
+	if opts.ImageInfo.Tag != "" {
+		mmMain["tag"] = opts.ImageInfo.Tag
+	}
+	if opts.ImageInfo.Repository != "" {
+		mmMain["repository_url"] = opts.ImageInfo.Repository
+	}
+	if opts.ImageInfo.Arch != "" {
+		mmMain["arch"] = opts.ImageInfo.Arch.ToOCIPlatform().Architecture
+	}
 	rootComponent := Component{
-		BOMRef:     fmt.Sprintf("pkg:apk/%s", opts.OS.ID),
+		BOMRef: purl.NewPackageURL(
+			purl.TypeOCI, "", opts.ImageInfo.Name, opts.ImageInfo.Digest,
+			purl.QualifiersFromMap(mmMain), "",
+		).String(),
 		Name:       opts.OS.Name,
 		Version:    opts.OS.Version,
 		Type:       "operating-system",
diff --git a/pkg/sbom/generator/spdx/spdx.go b/pkg/sbom/generator/spdx/spdx.go
index 599da19..039515a 100644
--- a/pkg/sbom/generator/spdx/spdx.go
+++ b/pkg/sbom/generator/spdx/spdx.go
@@ -18,12 +18,13 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
-	"path/filepath"
+	"strings"
 
 	"github.com/google/uuid"
+	"sigs.k8s.io/release-utils/version"
 
 	"chainguard.dev/apko/pkg/sbom/options"
-	"chainguard.dev/apko/pkg/sbom/purl"
+	purl "github.com/package-url/packageurl-go"
 )
 
 const NOASSERTION = "NOASSERTION"
@@ -51,7 +52,7 @@ func (sx *SPDX) Generate(opts *options.Options, path string) error {
 		CreationInfo: CreationInfo{
 			Created: "1970-01-01T00:00:00Z",
 			Creators: []string{
-				"Tool: apko (v0.0.0)",
+				fmt.Sprintf("Tool: apko (%s)", version.GetVersionInfo().GitVersion),
 				"Organization: Chainguard, Inc",
 			},
 			LicenseListVersion: "3.16",
@@ -62,21 +63,52 @@ func (sx *SPDX) Generate(opts *options.Options, path string) error {
 		Relationships: []Relationship{},
 	}
 
+	mainPkgID := "SPDXRef-Package-apko-os-layer-" + uuid.NewString()
+	if opts.ImageInfo.Reference != "" {
+		x := ""
+		if !strings.Contains(opts.ImageInfo.Reference, "/") {
+			x = "index.docker.io/library/"
+		}
+		mainPkgID = fmt.Sprintf("SPDXRef-%s%s", x, opts.ImageInfo.Reference)
+	}
+
+	// Main ackage purl
+	mmMain := map[string]string{}
+	if opts.ImageInfo.Tag != "" {
+		mmMain["tag"] = opts.ImageInfo.Tag
+	}
+	if opts.ImageInfo.Repository != "" {
+		mmMain["repository_url"] = opts.ImageInfo.Repository
+	}
+	if opts.ImageInfo.Arch != "" {
+		mmMain["arch"] = opts.ImageInfo.Arch.ToOCIPlatform().Architecture
+	}
+
 	mainPackage := Package{
-		ID:               "SPDXRef-Package-apko-os-layer-" + uuid.NewString(),
+		ID:               mainPkgID,
 		Name:             "apko-OS-Layer",
 		Version:          opts.OS.Version,
 		FilesAnalyzed:    false,
 		LicenseConcluded: NOASSERTION,
 		LicenseDeclared:  NOASSERTION,
 		Description:      "",
-		DownloadLocation: NOASSERTION, // switch when https://gitlab.alpinelinux.org/alpine/go/-/merge_requests/4
+		DownloadLocation: NOASSERTION,
 		Originator:       "",
 		SourceInfo:       "",
 		CopyrightText:    NOASSERTION,
 		Checksums:        []Checksum{},
-		ExternalRefs:     []ExternalRef{},
+		ExternalRefs: []ExternalRef{
+			{
+				Category: "PACKAGE_MANAGER",
+				Type:     "purl",
+				Locator: purl.NewPackageURL(
+					purl.TypeOCI, "", opts.ImageInfo.Name, opts.ImageInfo.Digest,
+					purl.QualifiersFromMap(mmMain), "",
+				).String(),
+			},
+		},
 	}
+	mm := map[string]string{"arch": opts.ImageInfo.Arch.ToAPK()}
 
 	doc.Packages = append(doc.Packages, mainPackage)
 	doc.DocumentDescribes = []string{mainPackage.ID}
@@ -91,21 +123,23 @@ func (sx *SPDX) Generate(opts *options.Options, path string) error {
 			LicenseConcluded: pkg.License,
 			LicenseDeclared:  NOASSERTION,
 			Description:      pkg.Description,
-			//DownloadLocation: pkg.,
-			Originator:    pkg.Maintainer,
-			SourceInfo:    "",
-			CopyrightText: NOASSERTION,
+			DownloadLocation: pkg.URL,
+			Originator:       pkg.Maintainer,
+			SourceInfo:       "Package info from apk database",
+			CopyrightText:    NOASSERTION,
 			Checksums: []Checksum{
 				{
 					Algorithm: "SHA1",
-					Value:     fmt.Sprintf("%x", pkg.Checksum[1:]),
+					Value:     fmt.Sprintf("%x", pkg.Checksum),
 				},
 			},
 			ExternalRefs: []ExternalRef{
 				{
 					Category: "PACKAGE_MANAGER",
-					Locator:  purl.Package(opts.OS.ID, pkg),
-					Type:     "purl",
+					Locator: purl.NewPackageURL(
+						"apk", opts.OS.ID, pkg.Name, pkg.Version,
+						purl.QualifiersFromMap(mm), "").String(),
+					Type: "purl",
 				},
 			},
 		}
@@ -120,7 +154,7 @@ func (sx *SPDX) Generate(opts *options.Options, path string) error {
 		})
 	}
 
-	out, err := os.Create(filepath.Join(path, fmt.Sprintf("%s.%s", opts.FileName, sx.Ext())))
+	out, err := os.Create(path)
 	if err != nil {
 		return fmt.Errorf("opening SBOM path %s for writing: %w", path, err)
 	}
diff --git a/pkg/sbom/options/options.go b/pkg/sbom/options/options.go
index 353556b..bd5339a 100644
--- a/pkg/sbom/options/options.go
+++ b/pkg/sbom/options/options.go
@@ -14,18 +14,22 @@
 
 package options
 
-import "gitlab.alpinelinux.org/alpine/go/pkg/repository"
+import (
+	"chainguard.dev/apko/pkg/build/types"
+	"gitlab.alpinelinux.org/alpine/go/pkg/repository"
+)
 
 type Options struct {
-	OS struct {
-		Name    string
-		ID      string
-		Version string
-	}
+	OS OSInfo
+
+	ImageInfo ImageInfo
 
 	// Working directory,inherited from buid context
 	WorkDir string
 
+	// The reference of the generated image. Used for naming and purls
+	ImageReference string
+
 	// OutputDir is the directory where the sboms will be written
 	OutputDir string
 
@@ -38,3 +42,18 @@ type Options struct {
 	// Packages is alist of packages which will be listed in the SBOM
 	Packages []*repository.Package
 }
+
+type OSInfo struct {
+	Name    string
+	ID      string
+	Version string
+}
+
+type ImageInfo struct {
+	Reference  string
+	Tag        string
+	Name       string
+	Repository string
+	Digest     string
+	Arch       types.Architecture
+}
diff --git a/pkg/sbom/purl/purl.go b/pkg/sbom/purl/purl.go
deleted file mode 100644
index 8c1e8fa..0000000
--- a/pkg/sbom/purl/purl.go
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright 2022 Chainguard, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package purl
-
-import (
-	"fmt"
-
-	"gitlab.alpinelinux.org/alpine/go/pkg/repository"
-)
-
-func Package(ns string, pkg *repository.Package) string {
-	return fmt.Sprintf("pkg:apk/%s/%s", ns, pkg.Name)
-}
-
-func Versioned(ns string, pkg *repository.Package) string {
-	return fmt.Sprintf("pkg:apk/%s/%s@%s", ns, pkg.Name, pkg.Version)
-}
diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go
index 9d87079..5e67899 100644
--- a/pkg/sbom/sbom.go
+++ b/pkg/sbom/sbom.go
@@ -33,17 +33,14 @@ const (
 )
 
 var DefaultOptions = options.Options{
-	OS: struct {
-		Name    string
-		ID      string
-		Version string
-	}{
+	OS: options.OSInfo{
 		ID:      "alpine",
 		Name:    "Alpine Linux",
 		Version: "Unknown",
 	},
-	FileName: "sbom",
-	Formats:  []string{"spdx", "cyclonedx"},
+	ImageInfo: options.ImageInfo{},
+	FileName:  "sbom",
+	Formats:   []string{"spdx", "cyclonedx"},
 }
 
 type SBOM struct {


Test patch:
diff --git a/pkg/sbom/generator/spdx/spdx_test.go b/pkg/sbom/generator/spdx/spdx_test.go
index 69322fd..0c7531d 100644
--- a/pkg/sbom/generator/spdx/spdx_test.go
+++ b/pkg/sbom/generator/spdx/spdx_test.go
@@ -56,9 +56,10 @@ var testOpts = &options.Options{
 func TestGenerate(t *testing.T) {
 	dir := t.TempDir()
 	sx := New()
-	err := sx.Generate(testOpts, dir)
+	path := filepath.Join(dir, testOpts.FileName+"."+sx.Ext())
+	err := sx.Generate(testOpts, path)
 	require.NoError(t, err)
-	require.FileExists(t, filepath.Join(dir, "sbom.spdx.json"))
+	require.FileExists(t, path)
 }
 
 // To run TestValidateSPDX, point SPDX_TOOLS_JAR to the SPDX tools
@@ -72,10 +73,11 @@ func TestValidateSPDX(t *testing.T) {
 	}
 	dir := t.TempDir()
 	sx := New()
+	path := filepath.Join(dir, testOpts.FileName+"."+sx.Ext())
 	err := sx.Generate(testOpts, dir)
 	require.NoError(t, err)
-	require.FileExists(t, filepath.Join(dir, "sbom.spdx.json"))
+	require.FileExists(t, path)
 	require.NoError(t, command.New(
-		"java", "-jar", jarPath, "Verify", filepath.Join(dir, "sbom.spdx.json"),
+		"java", "-jar", jarPath, "Verify", path,
 	).RunSuccess())
 }