Skip to content

Commit

Permalink
Add support for writing SBOMs when the build.Result is signed.
Browse files Browse the repository at this point in the history
This adds functionality that enables the default publisher to
publish SBOMs (and later signatures and attestations) when the
`build.Result` is an `oci.SignedEntity`.

This also changes the `gobuild` logic to start producing
`oci.Signed*` as its `build.Result`s, so when executed we get an
SBOM for each architecture image.

For example, see the "Published SBOM" lines below:

```shell
2021/11/19 19:24:50 Using base gcr.io/distroless/static:nonroot for github.com/google/ko
2021/11/19 19:24:51 Building github.com/google/ko for linux/amd64
2021/11/19 19:24:52 Building github.com/google/ko for linux/arm64
2021/11/19 19:24:57 Publishing ghcr.io/mattmoor/ko:latest
2021/11/19 19:24:58 existing blob: sha256:c78c74e7bb4a511f7d31061fbf140d55d5549a62d33cdbdf0c57ffe43603bbeb
2021/11/19 19:24:58 existing blob: sha256:4aa59d0bf53d4190174fbbfa3e9b15fdab72e5a95077025abfa8435ccafa2920
2021/11/19 19:24:58 ghcr.io/mattmoor/ko:sha256-d2bc030f5ed083d5e6a30a7969c9a8e599511b8d7a6e20695bf5ea029b6e2c3f.sbom: digest: sha256:c67ec671aaa82902e619883a7ac7486e6f9af36653449e2eb030ba273fe5a022 size: 348
2021/11/19 19:24:58 Published SBOM ghcr.io/mattmoor/ko:sha256-d2bc030f5ed083d5e6a30a7969c9a8e599511b8d7a6e20695bf5ea029b6e2c3f.sbom
2021/11/19 19:24:58 existing blob: sha256:c78c74e7bb4a511f7d31061fbf140d55d5549a62d33cdbdf0c57ffe43603bbeb
2021/11/19 19:24:58 existing blob: sha256:4aa59d0bf53d4190174fbbfa3e9b15fdab72e5a95077025abfa8435ccafa2920
2021/11/19 19:24:59 ghcr.io/mattmoor/ko:sha256-b74c230f20efd94981e5fd823bacc23cbd71055a1b3b6a0893152b398c67743b.sbom: digest: sha256:c67ec671aaa82902e619883a7ac7486e6f9af36653449e2eb030ba273fe5a022 size: 348
2021/11/19 19:24:59 Published SBOM ghcr.io/mattmoor/ko:sha256-b74c230f20efd94981e5fd823bacc23cbd71055a1b3b6a0893152b398c67743b.sbom
2021/11/19 19:24:59 existing blob: sha256:3f7e3c6765a6abc682cd40ea256fbea5c1d4debbc07659efbc0dedc13eee0da6
2021/11/19 19:24:59 existing blob: sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542
2021/11/19 19:24:59 existing blob: sha256:e8614d09b7bebabd9d8a450f44e88a8807c98a438a2ddd63146865286b132d1b
2021/11/19 19:24:59 existing blob: sha256:7067b1bc6f9ce59f3a4ed2216946ebbb27a4f7a102f55d96c6af1dc90e77b510
2021/11/19 19:25:00 ghcr.io/mattmoor/ko@sha256:d2bc030f5ed083d5e6a30a7969c9a8e599511b8d7a6e20695bf5ea029b6e2c3f: digest: sha256:d2bc030f5ed083d5e6a30a7969c9a8e599511b8d7a6e20695bf5ea029b6e2c3f size: 751
2021/11/19 19:25:01 existing blob: sha256:250c06f7c38e52dc77e5c7586c3e40280dc7ff9bb9007c396e06d96736cf8542
2021/11/19 19:25:02 pushed blob: sha256:121c637d5c84562b51404a6f71c1f995ad059740293a3911a0dc33eb223e41a4
2021/11/19 19:25:02 pushed blob: sha256:859e03b7461b2a512159493ef1504d2859ed37c05ed1ef781ff98394ea4799b5
2021/11/19 19:25:02 pushed blob: sha256:d1b55c3db0f16b5056776c6d2c279efd16d28dbf1aae3eef1f3f9b7551d1f490
2021/11/19 19:25:03 ghcr.io/mattmoor/ko@sha256:b74c230f20efd94981e5fd823bacc23cbd71055a1b3b6a0893152b398c67743b: digest: sha256:b74c230f20efd94981e5fd823bacc23cbd71055a1b3b6a0893152b398c67743b size: 751
2021/11/19 19:25:03 ghcr.io/mattmoor/ko:latest: digest: sha256:e4466a7dd9be66c7c1b43a8ecc19247041ece232407a14e3d6ea3c51d2561a71 size: 529
2021/11/19 19:25:03 Published ghcr.io/mattmoor/ko@sha256:e4466a7dd9be66c7c1b43a8ecc19247041ece232407a14e3d6ea3c51d2561a71
ghcr.io/mattmoor/ko@sha256:e4466a7dd9be66c7c1b43a8ecc19247041ece232407a14e3d6ea3c51d2561a71
```

The "SBOM" being attached in this change is the raw output of `go version -m`,
which we will convert to one of the standard formats in a subsequent change.
  • Loading branch information
mattmoor committed Nov 20, 2021
1 parent 2fbc908 commit e69bf48
Show file tree
Hide file tree
Showing 170 changed files with 14,631 additions and 307 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/kind-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."$REGISTRY_NAME:$REGISTRY_PORT"]
endpoint = ["http://$REGISTRY_NAME:$REGISTRY_PORT"]
EOF
- uses: helm/kind-action@v1.2.0
with:
cluster_name: kind
Expand All @@ -61,6 +62,11 @@ jobs:
run: |
kubectl wait --timeout=2m --for=condition=Ready nodes --all
- name: Install Cosign
uses: sigstore/cosign-installer@main
with:
cosign-release: 'v1.3.1'

- name: Run Smoke Test
run: |
# Test with kind load
Expand All @@ -73,6 +79,28 @@ jobs:
kubectl wait --timeout=60s --for=condition=Ready pod/kodata
kubectl delete pod kodata
- name: Check SBOM
run: |
set -o pipefail
IMAGE=$(ko publish ./test)
SBOM=$(cosign download sbom ${IMAGE})
KO_DEPS=$(ko deps ${IMAGE})
echo '::group:: SBOM'
echo "${SBOM}"
echo '::endgroup::'
echo '::group:: ko deps'
echo "${KO_DEPS}"
echo '::endgroup::'
if [ "${SBOM}" != "${KO_DEPS}" ] ; then
echo Wanted SBOM and 'ko deps' to match, got differences!
exit 1
fi
- name: Collect logs
if: ${{ always() }}
run: |
Expand Down
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@ require (
github.com/containerd/stargz-snapshotter/estargz v0.10.0
github.com/docker/docker v20.10.10+incompatible
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960
github.com/evanphx/json-patch/v5 v5.5.0 // indirect
github.com/fsnotify/fsnotify v1.5.1
github.com/go-training/helloworld v0.0.0-20200225145412-ba5f4379d78b
github.com/google/go-cmp v0.5.6
github.com/google/go-containerregistry v0.7.0
github.com/mattmoor/dep-notify v0.0.0-20190205035814-a45dec370a17
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/opencontainers/image-spec v1.0.2-0.20210730191737-8e42a01fb1b7
github.com/sigstore/cosign v1.3.2-0.20211120003522-90e2dcfe7b92
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.9.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.7
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
k8s.io/apimachinery v0.22.3
Expand Down
1,125 changes: 1,117 additions & 8 deletions go.sum

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pkg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ type Interface interface {
Build(context.Context, string) (Result, error)
}

// Result represents the product of a Build. This is usually a v1.Image or v1.ImageIndex.
// Result represents the product of a Build.
// This is generally one of:
// - v1.Image (or oci.SignedImage), or
// - v1.ImageIndex (or oci.SignedImageIndex)
type Result interface {
MediaType() (types.MediaType, error)
Size() (int64, error)
Expand Down
91 changes: 71 additions & 20 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ import (
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
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"
"golang.org/x/tools/go/packages"
)

Expand All @@ -53,6 +57,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) ([]byte, types.MediaType, error)

type platformMatcher struct {
spec string
Expand All @@ -64,7 +69,9 @@ type gobuild struct {
creationTime v1.Time
kodataCreationTime v1.Time
build builder
sbom sbomber
disableOptimizations bool
disableSBOM bool
trimpath bool
buildConfigs map[string]Config
platformMatcher *platformMatcher
Expand All @@ -80,7 +87,9 @@ type gobuildOpener struct {
creationTime v1.Time
kodataCreationTime v1.Time
build builder
sbom sbomber
disableOptimizations bool
disableSBOM bool
trimpath bool
buildConfigs map[string]Config
platform string
Expand All @@ -101,7 +110,9 @@ func (gbo *gobuildOpener) Open() (Interface, error) {
creationTime: gbo.creationTime,
kodataCreationTime: gbo.kodataCreationTime,
build: gbo.build,
sbom: gbo.sbom,
disableOptimizations: gbo.disableOptimizations,
disableSBOM: gbo.disableSBOM,
trimpath: gbo.trimpath,
buildConfigs: gbo.buildConfigs,
labels: gbo.labels,
Expand All @@ -119,6 +130,7 @@ 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,
}

Expand Down Expand Up @@ -249,6 +261,20 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con
return file, nil
}

func sbom(ctx context.Context, file string) ([]byte, types.MediaType, error) {
sbom := bytes.NewBuffer(nil)
cmd := exec.CommandContext(ctx, "go", "version", "-m", file)
cmd.Stdout = sbom
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return nil, "", err
}

// TODO(imjasonh): Turn the output of `go version -m` on
// file into a standard format and attach here.
return sbom.Bytes(), "application/vnd.go-mod", 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.
Expand Down Expand Up @@ -578,7 +604,7 @@ func (g *gobuild) configForImportPath(ip string) Config {
return config
}

func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, platform *v1.Platform) (v1.Image, error) {
func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, platform *v1.Platform) (oci.SignedImage, error) {
ref := newRef(refStr)

cf, err := base.ConfigFile()
Expand Down Expand Up @@ -689,9 +715,32 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl

empty := v1.Time{}
if g.creationTime != empty {
return mutate.CreatedAt(image, g.creationTime)
image, err = mutate.CreatedAt(image, g.creationTime)
if err != nil {
return nil, err
}
}

si := signed.Image(image)

if !g.disableSBOM {
sbom, mt, err := g.sbom(ctx, file)
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 image, nil

return si, nil
}

// Append appPath to the PATH environment variable, if it exists. Otherwise,
Expand Down Expand Up @@ -735,6 +784,19 @@ func (g *gobuild) Build(ctx context.Context, s string) (Result, error) {
return nil, err
}

// Annotate the base image we pass to the build function with
// annotations indicating the digest (and possibly tag) of the
// base image. This will be inherited by the image produced.
if mt != types.DockerManifestList {
anns := map[string]string{
specsv1.AnnotationBaseImageDigest: baseDigest.String(),
}
if _, ok := baseRef.(name.Tag); ok {
anns[specsv1.AnnotationBaseImageName] = baseRef.Name()
}
base = mutate.Annotations(base, anns).(Result)
}

var res Result
switch mt {
case types.OCIImageIndex, types.DockerManifestList:
Expand All @@ -755,31 +817,18 @@ func (g *gobuild) Build(ctx context.Context, s string) (Result, error) {
if err != nil {
return nil, err
}

// Annotate the image or index with base image information.
// (Docker manifest lists don't support annotations)
if mt != types.DockerManifestList {
anns := map[string]string{
specsv1.AnnotationBaseImageDigest: baseDigest.String(),
}
if _, ok := baseRef.(name.Tag); ok {
anns[specsv1.AnnotationBaseImageName] = baseRef.Name()
}
res = mutate.Annotations(res, anns).(Result)
}

return res, nil
}

// TODO(#192): Do these in parallel?
func (g *gobuild) buildAll(ctx context.Context, ref string, baseIndex v1.ImageIndex) (v1.ImageIndex, error) {
func (g *gobuild) buildAll(ctx context.Context, ref string, baseIndex v1.ImageIndex) (oci.SignedImageIndex, error) {
im, err := baseIndex.IndexManifest()
if err != nil {
return nil, err
}

// Build an image for each child from the base and append it to a new index to produce the result.
adds := []mutate.IndexAddendum{}
adds := []ocimutate.IndexAddendum{}
for _, desc := range im.Manifests {
// Nested index is pretty rare. We could support this in theory, but return an error for now.
if desc.MediaType != types.OCIManifestSchema1 && desc.MediaType != types.DockerManifestSchema2 {
Expand All @@ -798,7 +847,7 @@ func (g *gobuild) buildAll(ctx context.Context, ref string, baseIndex v1.ImageIn
if err != nil {
return nil, err
}
adds = append(adds, mutate.IndexAddendum{
adds = append(adds, ocimutate.IndexAddendum{
Add: img,
Descriptor: v1.Descriptor{
URLs: desc.URLs,
Expand All @@ -813,8 +862,10 @@ func (g *gobuild) buildAll(ctx context.Context, ref string, baseIndex v1.ImageIn
if err != nil {
return nil, err
}
idx := mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), baseType)
idx := ocimutate.AppendManifests(mutate.IndexMediaType(empty.Index, baseType), adds...)

// TODO(mattmoor): If we want to attach anything (e.g. signatures, attestations, SBOM)
// at the index level, we would do it here!
return idx, nil
}

Expand Down
Loading

0 comments on commit e69bf48

Please sign in to comment.