From 3edb68b27326d06d09bfd977735d4b5604eca7b2 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Mon, 22 Nov 2021 14:19:43 -0800 Subject: [PATCH] Connect SBOMs with SPDX support. (#511) * Connect SBOMs with SPDX support. This combines Jason's SPDX stuff and my SBOM stuff to support SPDX-based SBOMs by default instead of our `go version -m` invention. * Make ko deps use SPDX by default --- .github/workflows/kind-e2e.yaml | 2 +- doc/ko_apply.md | 2 +- doc/ko_build.md | 2 +- doc/ko_create.md | 2 +- doc/ko_deps.md | 2 +- doc/ko_resolve.md | 2 +- doc/ko_run.md | 2 +- pkg/build/gobuild.go | 60 ++++++++++++++++++++++----------- pkg/build/gobuild_test.go | 2 +- pkg/build/options.go | 20 ++++++++++- pkg/commands/deps.go | 2 +- pkg/commands/options/build.go | 4 +-- pkg/commands/resolver.go | 4 +++ 13 files changed, 74 insertions(+), 32 deletions(-) diff --git a/.github/workflows/kind-e2e.yaml b/.github/workflows/kind-e2e.yaml index 1330d8919f..4c16cfc935 100644 --- a/.github/workflows/kind-e2e.yaml +++ b/.github/workflows/kind-e2e.yaml @@ -84,7 +84,7 @@ jobs: run: | set -o pipefail - IMAGE=$(ko publish ./test) + IMAGE=$(ko build ./test) SBOM=$(cosign download sbom ${IMAGE}) KO_DEPS=$(ko deps ${IMAGE}) diff --git a/doc/ko_apply.md b/doc/ko_apply.md index 8a0b79d276..55c7cac738 100644 --- a/doc/ko_apply.md +++ b/doc/ko_apply.md @@ -72,7 +72,7 @@ ko apply -f FILENAME [flags] --push Push images to KO_DOCKER_REPO (default true) -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (DEPRECATED) - --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "go.version-m") + --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, go.version-m). (default "spdx") -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2) -s, --server string The address and port of the Kubernetes API server (DEPRECATED) --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. diff --git a/doc/ko_build.md b/doc/ko_build.md index 0d958af4dd..1adc36c5a7 100644 --- a/doc/ko_build.md +++ b/doc/ko_build.md @@ -55,7 +55,7 @@ ko build IMPORTPATH... [flags] --platform string Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]* -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) - --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "go.version-m") + --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, go.version-m). (default "spdx") --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) --tarball string File to save images tarballs diff --git a/doc/ko_create.md b/doc/ko_create.md index f14f9ad26c..bace61694d 100644 --- a/doc/ko_create.md +++ b/doc/ko_create.md @@ -72,7 +72,7 @@ ko create -f FILENAME [flags] --push Push images to KO_DOCKER_REPO (default true) -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (DEPRECATED) - --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "go.version-m") + --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, go.version-m). (default "spdx") -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2) -s, --server string The address and port of the Kubernetes API server (DEPRECATED) --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. diff --git a/doc/ko_deps.md b/doc/ko_deps.md index 086a66f4ee..424def55c8 100644 --- a/doc/ko_deps.md +++ b/doc/ko_deps.md @@ -24,7 +24,7 @@ ko deps IMAGE [flags] ``` -h, --help help for deps - --sbom string Format for SBOM output (default "go.version-m") + --sbom string Format for SBOM output (supports: spdx, go.version-m). (default "spdx") ``` ### SEE ALSO diff --git a/doc/ko_resolve.md b/doc/ko_resolve.md index f5926c35d2..7ec68c95a3 100644 --- a/doc/ko_resolve.md +++ b/doc/ko_resolve.md @@ -52,7 +52,7 @@ ko resolve -f FILENAME [flags] -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. - --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "go.version-m") + --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, go.version-m). (default "spdx") -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2) --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) diff --git a/doc/ko_run.md b/doc/ko_run.md index 17de4a7425..d09e19857a 100644 --- a/doc/ko_run.md +++ b/doc/ko_run.md @@ -42,7 +42,7 @@ ko run IMPORTPATH [flags] --platform string Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]* -P, --preserve-import-paths Whether to preserve the full import path after KO_DOCKER_REPO. --push Push images to KO_DOCKER_REPO (default true) - --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload). (default "go.version-m") + --sbom string The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, go.version-m). (default "spdx") --tag-only Include tags but not digests in resolved image references. Useful when digests are not preserved when images are repopulated. -t, --tags strings Which tags to use for the produced image instead of the default 'latest' tag (may not work properly with --base-import-paths or --bare). (default [latest]) --tarball string File to save images tarballs diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 19fb1a170b..475a0101de 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -41,11 +41,13 @@ import ( "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/ko/internal/sbom" specsv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sigstore/cosign/pkg/oci" ocimutate "github.com/sigstore/cosign/pkg/oci/mutate" "github.com/sigstore/cosign/pkg/oci/signed" "github.com/sigstore/cosign/pkg/oci/static" + ctypes "github.com/sigstore/cosign/pkg/types" "golang.org/x/tools/go/packages" ) @@ -57,7 +59,7 @@ const ( type GetBase func(context.Context, string) (name.Reference, Result, error) type builder func(context.Context, string, string, v1.Platform, Config) (string, error) -type sbomber func(context.Context, string, string) ([]byte, types.MediaType, error) +type sbomber func(context.Context, string, string, v1.Image) ([]byte, types.MediaType, error) type platformMatcher struct { spec string @@ -71,7 +73,6 @@ type gobuild struct { build builder sbom sbomber disableOptimizations bool - disableSBOM bool trimpath bool buildConfigs map[string]Config platformMatcher *platformMatcher @@ -89,7 +90,6 @@ type gobuildOpener struct { build builder sbom sbomber disableOptimizations bool - disableSBOM bool trimpath bool buildConfigs map[string]Config platform string @@ -112,7 +112,6 @@ func (gbo *gobuildOpener) Open() (Interface, error) { build: gbo.build, sbom: gbo.sbom, disableOptimizations: gbo.disableOptimizations, - disableSBOM: gbo.disableSBOM, trimpath: gbo.trimpath, buildConfigs: gbo.buildConfigs, labels: gbo.labels, @@ -130,8 +129,8 @@ func (gbo *gobuildOpener) Open() (Interface, error) { func NewGo(ctx context.Context, dir string, options ...Option) (Interface, error) { gbo := &gobuildOpener{ build: build, - sbom: sbom, dir: dir, + sbom: spdx("(none)"), } for _, option := range options { @@ -261,7 +260,7 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con return file, nil } -func sbom(ctx context.Context, file string, appPath string) ([]byte, types.MediaType, error) { +func goversionm(ctx context.Context, file string, appPath string, _ v1.Image) ([]byte, types.MediaType, error) { sbom := bytes.NewBuffer(nil) cmd := exec.CommandContext(ctx, "go", "version", "-m", file) cmd.Stdout = sbom @@ -270,14 +269,31 @@ func sbom(ctx context.Context, file string, appPath string) ([]byte, types.Media return nil, "", err } - // TODO(imjasonh): Turn the output of `go version -m` on - // file into a standard format and attach here. - // In order to get deterministics SBOMs replace our randomized // file name with the path the app will get inside of the container. return []byte(strings.Replace(sbom.String(), file, appPath, 1)), "application/vnd.go.version-m", nil } +func spdx(version string) sbomber { + return func(ctx context.Context, file string, appPath string, img v1.Image) ([]byte, types.MediaType, error) { + b, _, err := goversionm(ctx, file, appPath, img) + if err != nil { + return nil, "", err + } + + cfg, err := img.ConfigFile() + if err != nil { + return nil, "", err + } + + b, err = sbom.GenerateSPDX(version, cfg.Created.Time, b) + if err != nil { + return nil, "", err + } + return b, ctypes.SPDXMediaType, nil + } +} + // buildEnv creates the environment variables used by the `go build` command. // From `os/exec.Cmd`: If Env contains duplicate environment keys, only the last // value in the slice for each duplicate key is used. @@ -724,19 +740,23 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl } } - if g.disableSBOM { - return signed.Image(image), nil - } + si := signed.Image(image) - sbom, mt, err := g.sbom(ctx, file, appPath) - if err != nil { - return nil, err - } - f, err := static.NewFile(sbom, static.WithLayerMediaType(mt)) - if err != nil { - return nil, err + if g.sbom != nil { + sbom, mt, err := g.sbom(ctx, file, appPath, image) + if err != nil { + return nil, err + } + f, err := static.NewFile(sbom, static.WithLayerMediaType(mt)) + if err != nil { + return nil, err + } + si, err = ocimutate.AttachFileToImage(si, "sbom", f) + if err != nil { + return nil, err + } } - return ocimutate.AttachFileToImage(signed.Image(image), "sbom", f) + return si, nil } // Append appPath to the PATH environment variable, if it exists. Otherwise, diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index 453f8fb393..f712adbb4d 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -392,7 +392,7 @@ func nilGetBase(_ context.Context, _ string) (name.Reference, Result, error) { const wantSBOM = "This is our fake SBOM" // A helper method we use to substitute for the default "build" method. -func fauxSBOM(_ context.Context, _ string, _ string) ([]byte, types.MediaType, error) { +func fauxSBOM(_ context.Context, _ string, _ string, _ v1.Image) ([]byte, types.MediaType, error) { return []byte(wantSBOM), "application/vnd.garbage", nil } diff --git a/pkg/build/options.go b/pkg/build/options.go index 2ed05e810d..322ec297a8 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -57,7 +57,7 @@ func WithDisabledOptimizations() Option { // WithDisabledSBOM is a functional option for disabling SBOM generation. func WithDisabledSBOM() Option { return func(gbo *gobuildOpener) error { - gbo.disableSBOM = true + gbo.sbom = nil return nil } } @@ -116,6 +116,24 @@ func withBuilder(b builder) Option { } } +// WithGoVersionSBOM is a functional option to direct ko to use +// go version -m for SBOM format. +func WithGoVersionSBOM() Option { + return func(gbo *gobuildOpener) error { + gbo.sbom = goversionm + return nil + } +} + +// WithSPDX is a functional option to direct ko to use +// SPDX for SBOM format. +func WithSPDX(version string) Option { + return func(gbo *gobuildOpener) error { + gbo.sbom = spdx(version) + return nil + } +} + // withSBOMber is a functional option for overriding the way SBOMs // are generated. func withSBOMber(sbom sbomber) Option { diff --git a/pkg/commands/deps.go b/pkg/commands/deps.go index c7f518dad7..3a6ed2d9ba 100644 --- a/pkg/commands/deps.go +++ b/pkg/commands/deps.go @@ -148,6 +148,6 @@ If the image was not built using ko, or if it was built without embedding depend // unreachable }, } - deps.Flags().StringVar(&sbomType, "sbom", "go.version-m", "Format for SBOM output") + deps.Flags().StringVar(&sbomType, "sbom", "spdx", "Format for SBOM output (supports: spdx, go.version-m).") topLevel.AddCommand(deps) } diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go index ce8a05a253..e1f7c26f8d 100644 --- a/pkg/commands/options/build.go +++ b/pkg/commands/options/build.go @@ -74,8 +74,8 @@ func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) { "The maximum number of concurrent builds (default GOMAXPROCS)") cmd.Flags().BoolVar(&bo.DisableOptimizations, "disable-optimizations", bo.DisableOptimizations, "Disable optimizations when building Go code. Useful when you want to interactively debug the created container.") - cmd.Flags().StringVar(&bo.SBOM, "sbom", "go.version-m", - "The SBOM media type to use (none will disable SBOM synthesis and upload).") + cmd.Flags().StringVar(&bo.SBOM, "sbom", "spdx", + "The SBOM media type to use (none will disable SBOM synthesis and upload, also supports: spdx, go.version-m).") cmd.Flags().StringVar(&bo.Platform, "platform", "", "Which platform to use when pulling a multi-platform base. Format: all | [/[/]][,platform]*") cmd.Flags().StringSliceVar(&bo.Labels, "image-label", []string{}, diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index 3609ea9bba..e561824cd4 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -102,6 +102,10 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { switch bo.SBOM { case "none": opts = append(opts, build.WithDisabledSBOM()) + case "spdx": + opts = append(opts, build.WithSPDX(version())) + case "go.version-m": + opts = append(opts, build.WithGoVersionSBOM()) } opts = append(opts, build.WithTrimpath(bo.Trimpath)) for _, lf := range bo.Labels {