From c6906af22dcd64371fc5e2269547b7ea126c7f02 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 12 May 2026 10:02:34 +0530 Subject: [PATCH] build: generate SBOMs for minirootfs --- internal/cli/build-minirootfs.go | 17 ++++++-- internal/cli/build_test.go | 31 +++++++++++++ pkg/build/sbom.go | 75 ++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/internal/cli/build-minirootfs.go b/internal/cli/build-minirootfs.go index 0429b7b96..f50b871f7 100644 --- a/internal/cli/build-minirootfs.go +++ b/internal/cli/build-minirootfs.go @@ -27,6 +27,7 @@ import ( "chainguard.dev/apko/pkg/build" "chainguard.dev/apko/pkg/build/types" "chainguard.dev/apko/pkg/options" + "chainguard.dev/apko/pkg/sbom/generator" "chainguard.dev/apko/pkg/tarfs" ) @@ -48,7 +49,7 @@ func buildMinirootFS() *cobra.Command { Example: ` apko build-minirootfs `, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return BuildMinirootFSCmd(cmd.Context(), + opts := []build.Option{ build.WithConfig(args[0], []string{}), build.WithExtraKeys(extraKeys), build.WithExtraBuildRepos(extraBuildRepos), @@ -60,7 +61,11 @@ func buildMinirootFS() *cobra.Command { build.WithArch(types.ParseArchitecture(buildArch)), build.WithIgnoreSignatures(ignoreSignatures), build.WithSizeLimits(sizeLimits), - ) + } + if sbomPath != "" { + opts = append(opts, build.WithSBOMGenerators(generator.Generators("spdx")...)) + } + return BuildMinirootFSCmd(cmd.Context(), opts...) }, } @@ -97,11 +102,17 @@ func BuildMinirootFSCmd(ctx context.Context, opts ...build.Option) error { } log.Debugf("building minirootfs %s", bc.TarballPath()) - layerTarGZ, _, err := bc.BuildLayer(ctx) + layerTarGZ, layer, err := bc.BuildLayer(ctx) if err != nil { return fmt.Errorf("failed to build layer image: %w", err) } log.Debugf("wrote minirootfs to %s", layerTarGZ) + if bc.WantSBOM() { + if _, err := bc.GenerateLayerSBOM(ctx, bc.Arch(), layer); err != nil { + return fmt.Errorf("generating sbom: %w", err) + } + } + return nil } diff --git a/internal/cli/build_test.go b/internal/cli/build_test.go index b429f9c00..5bd2c2f85 100644 --- a/internal/cli/build_test.go +++ b/internal/cli/build_test.go @@ -155,3 +155,34 @@ func TestBuildWithBase(t *testing.T) { require.Equal(t, want, got) } + +func TestBuildMinirootFSWritesSBOM(t *testing.T) { + ctx := context.Background() + tmp := t.TempDir() + + sbomPath := filepath.Join(tmp, "sboms") + require.NoError(t, os.MkdirAll(sbomPath, 0o750)) + + output := filepath.Join(tmp, "minirootfs.tar.gz") + err := cli.BuildMinirootFSCmd(ctx, + build.WithConfig(filepath.Join("testdata", "apko.yaml"), []string{}), + build.WithArch(types.ParseArchitecture("amd64")), + build.WithTarball(output), + build.WithSBOM(sbomPath), + build.WithSBOMGenerators(spdx.New()), + ) + require.NoError(t, err) + + require.FileExists(t, output) + + sbom := filepath.Join(sbomPath, "sbom-x86_64.spdx.json") + require.FileExists(t, sbom) + + data, err := os.ReadFile(sbom) + require.NoError(t, err) + + var doc map[string]any + require.NoError(t, json.Unmarshal(data, &doc)) + require.Equal(t, "SPDX-2.3", doc["spdxVersion"]) + require.NotEmpty(t, doc["packages"]) +} diff --git a/pkg/build/sbom.go b/pkg/build/sbom.go index 87eb240bd..4b945d49c 100644 --- a/pkg/build/sbom.go +++ b/pkg/build/sbom.go @@ -139,6 +139,81 @@ func (bc *Context) GenerateImageSBOM(ctx context.Context, arch types.Architectur return sboms, nil } +func (bc *Context) GenerateLayerSBOM(ctx context.Context, arch types.Architecture, layer v1.Layer) ([]types.SBOM, error) { + log := clog.FromContext(ctx).With("arch", arch.ToAPK()) + ctx = clog.WithLogger(ctx, log) + + _, span := otel.Tracer("apko").Start(ctx, "GenerateLayerSBOM") + defer span.End() + + if !bc.WantSBOM() { + log.Warnf("skipping SBOM generation") + return nil, nil + } + + bde, err := bc.GetBuildDateEpoch() + if err != nil { + return nil, fmt.Errorf("computing build date epoch: %w", err) + } + + layerDigest, err := layer.Digest() + if err != nil { + return nil, fmt.Errorf("getting %s layer digest: %w", arch, err) + } + + layerSize, err := layer.Size() + if err != nil { + return nil, fmt.Errorf("getting %s layer size: %w", arch, err) + } + + layerMediaType, err := layer.MediaType() + if err != nil { + return nil, fmt.Errorf("getting %s layer media type: %w", arch, err) + } + + s := newSBOM(ctx, bc.fs, bc.o, bc.ic, bde) + log.Debug("Generating layer SBOM") + + s.ImageInfo.Layers = []v1.Descriptor{{ + MediaType: layerMediaType, + Size: layerSize, + Digest: layerDigest, + }} + + info, err := fetchFSReleaseData(bc.fs) + if err != nil { + return nil, fmt.Errorf("reading release data: %w", err) + } + + s.OS.Name = info.Name + s.OS.ID = info.ID + s.OS.Version = info.VersionID + s.ImageInfo.Arch = arch + + pkgs, err := bc.apk.GetInstalled() + if err != nil { + return nil, fmt.Errorf("reading apk package index: %w", err) + } + + s.Packages = pkgs + + sboms := make([]types.SBOM, 0) + for _, gen := range bc.o.SBOMGenerators { + filename := filepath.Join(s.OutputDir, s.FileName+"."+gen.Ext()) + if err := gen.Generate(ctx, &s, filename); err != nil { + return nil, fmt.Errorf("generating %s sbom: %w", gen.Key(), err) + } + sboms = append(sboms, types.SBOM{ + Path: filename, + Format: gen.Key(), + PredicateType: gen.PredicateType(), + Arch: arch.String(), + Digest: layerDigest, + }) + } + return sboms, nil +} + type ReleaseData struct { ID string Name string