From 19cf9ee132ee91acba343883cfaaa18e5a1a7e56 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Fri, 14 Feb 2025 11:25:48 -0600 Subject: [PATCH] rebuild: make SBOM more reproducible Signed-off-by: Jason Hall --- pkg/cli/rebuild.go | 64 +++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/pkg/cli/rebuild.go b/pkg/cli/rebuild.go index 650499e38..afb1d86eb 100644 --- a/pkg/cli/rebuild.go +++ b/pkg/cli/rebuild.go @@ -3,15 +3,17 @@ package cli import ( "archive/tar" "compress/gzip" + "encoding/json" "errors" "fmt" "io" - "log" "os" + "strings" "time" goapk "chainguard.dev/apko/pkg/apk/apk" apko_types "chainguard.dev/apko/pkg/build/types" + "chainguard.dev/apko/pkg/sbom/generator/spdx" "chainguard.dev/melange/pkg/build" "chainguard.dev/melange/pkg/config" "chainguard.dev/melange/pkg/container/docker" @@ -41,7 +43,7 @@ func rebuild() *cobra.Command { } for _, a := range args { - cfg, pkginfo, err := getConfig(a) + cfg, pkginfo, cfgpkg, err := getConfig(a) if err != nil { return fmt.Errorf("failed to get config for %s: %v", a, err) } @@ -50,21 +52,23 @@ func rebuild() *cobra.Command { cfg.Environment.Contents.RuntimeRepositories = append(cfg.Environment.Contents.RuntimeRepositories, "https://packages.wolfi.dev/os") cfg.Environment.Contents.Keyring = append(cfg.Environment.Contents.Keyring, "https://packages.wolfi.dev/os/wolfi-signing.rsa.pub") - f, err := os.CreateTemp("", "melange-rebuild-*.") + f, err := os.Create(fmt.Sprintf("%s.yaml", cfg.Package.Name)) if err != nil { return fmt.Errorf("failed to create temporary file: %v", err) } if err := yaml.NewEncoder(f).Encode(cfg); err != nil { return fmt.Errorf("failed to encode stripped config: %v", err) } - log.Println("wrote stripped config to", f.Name()) + defer f.Close() + defer os.Remove(f.Name()) if err := BuildCmd(ctx, []apko_types.Architecture{apko_types.Architecture("amd64")}, // TODO configurable, or detect build.WithConfigFileRepositoryURL("https://github.com/wolfi-dev/os"), // TODO get this from the package SBOM - build.WithConfigFileRepositoryCommit("TODO"), // TODO get this from the package SBOM - build.WithConfigFileLicense("Apache-2.0"), // TODO get this from the package SBOM - build.WithBuildDate(time.Unix(pkginfo.BuildDate, 0).Format(time.RFC3339)), + build.WithNamespace("wolfi"), // TODO get this from the package SBOM + build.WithConfigFileRepositoryCommit(cfgpkg.Version), + build.WithConfigFileLicense(cfgpkg.LicenseDeclared), + build.WithBuildDate(time.Unix(pkginfo.BuildDate, 0).UTC().Format(time.RFC3339)), build.WithRunner(r), // TODO configurable build.WithConfig(f.Name())); err != nil { return fmt.Errorf("failed to rebuild %q: %v", a, err) @@ -76,19 +80,20 @@ func rebuild() *cobra.Command { } } -func getConfig(fn string) (*config.Configuration, *goapk.PackageInfo, error) { +func getConfig(fn string) (*config.Configuration, *goapk.PackageInfo, *spdx.Package, error) { f, err := os.Open(fn) if err != nil { - return nil, nil, fmt.Errorf("failed to open file %s: %v", fn, err) + return nil, nil, nil, fmt.Errorf("failed to open file %s: %v", fn, err) } defer f.Close() var cfg *config.Configuration var pkginfo *goapk.PackageInfo + var cfgpkg *spdx.Package gz, err := gzip.NewReader(f) if err != nil { - return nil, nil, fmt.Errorf("failed to create gzip reader: %v", err) + return nil, nil, nil, fmt.Errorf("failed to create gzip reader: %v", err) } defer gz.Close() tr := tar.NewReader(gz) @@ -96,40 +101,59 @@ func getConfig(fn string) (*config.Configuration, *goapk.PackageInfo, error) { hdr, err := tr.Next() if errors.Is(err, io.EOF) { if cfg == nil { - return nil, nil, fmt.Errorf("failed to find .melange.yaml in %s", fn) + return nil, nil, nil, fmt.Errorf("failed to find .melange.yaml in %s", fn) } if pkginfo == nil { - return nil, nil, fmt.Errorf("failed to find .PKGINFO in %s", fn) + return nil, nil, nil, fmt.Errorf("failed to find .PKGINFO in %s", fn) } - return nil, nil, fmt.Errorf("failed to find necessary rebuild information in %s", fn) + if cfgpkg == nil { + return nil, nil, nil, fmt.Errorf("failed to find SBOM in %s", fn) + } + return nil, nil, nil, fmt.Errorf("failed to find necessary rebuild information in %s", fn) } else if err != nil { - return nil, nil, fmt.Errorf("failed to read tar header: %v", err) + return nil, nil, nil, fmt.Errorf("failed to read tar header: %v", err) } switch hdr.Name { case ".melange.yaml": cfg = new(config.Configuration) if err := yaml.NewDecoder(io.LimitReader(tr, hdr.Size)).Decode(cfg); err != nil { - return nil, nil, fmt.Errorf("failed to decode .melange.yaml: %v", err) + return nil, nil, nil, fmt.Errorf("failed to decode .melange.yaml: %v", err) } case ".PKGINFO": i, err := ini.ShadowLoad(tr) if err != nil { - return nil, nil, fmt.Errorf("failed to load .PKGINFO: %v", err) + return nil, nil, nil, fmt.Errorf("failed to load .PKGINFO: %v", err) } pkginfo = new(goapk.PackageInfo) if err = i.MapTo(pkginfo); err != nil { - return nil, nil, fmt.Errorf("failed to map .PKGINFO: %v", err) + return nil, nil, nil, fmt.Errorf("failed to map .PKGINFO: %v", err) + } + + case fmt.Sprintf("var/lib/db/sbom/%s-%s.spdx.json", pkginfo.Name, pkginfo.Version), + fmt.Sprintf("var/lib/db/sbom/%s-%s-r%d.spdx.json", cfg.Package.Name, cfg.Package.Version, cfg.Package.Epoch): + doc := new(spdx.Document) + if err := json.NewDecoder(io.LimitReader(tr, hdr.Size)).Decode(doc); err != nil { + return nil, nil, nil, fmt.Errorf("failed to decode SBOM: %v", err) + } + + for _, p := range doc.Packages { + if strings.HasSuffix(p.Name, ".yaml") { + cfgpkg = &p + break + } + } + if cfgpkg == nil { + return nil, nil, nil, fmt.Errorf("failed to find config package info in SBOM: %v", err) } default: - // TODO: Get the SBOM, since we need some info from it too. continue } - if cfg != nil && pkginfo != nil { - return cfg, pkginfo, nil + if cfg != nil && pkginfo != nil && cfgpkg != nil { + return cfg, pkginfo, cfgpkg, nil } } // unreachable