Skip to content

Commit

Permalink
Generate ko deps in SPDX format (ko-build#507)
Browse files Browse the repository at this point in the history
* WIP: generate ko deps in SPDX format

- copy out a bunch of BuildInfo stuff that will land in 1.18

* review comments

* have deps take --sbom flag more like Matt's new publish-time flag
  • Loading branch information
imjasonh authored Nov 22, 2021
1 parent 6d06913 commit af2ff52
Show file tree
Hide file tree
Showing 19 changed files with 954 additions and 82 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/kind-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ jobs:
set -o pipefail
IMAGE=$(ko publish ./test)
# Strip the first line of each which contains garbage filenames.
SBOM=$(cosign download sbom ${IMAGE} | sed 1d)
KO_DEPS=$(ko deps ${IMAGE} | sed 1d)
SBOM=$(cosign download sbom ${IMAGE})
KO_DEPS=$(ko deps ${IMAGE})
echo '::group:: SBOM'
echo "${SBOM}"
Expand Down
3 changes: 2 additions & 1 deletion doc/ko_deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ ko deps IMAGE [flags]
### Options

```
-h, --help help for deps
-h, --help help for deps
--sbom string Format for SBOM output (default "go.version-m")
```

### SEE ALSO
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
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/tools v0.1.7
golang.org/x/tools v0.1.8-0.20211015140901-98f6e0395b11
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
k8s.io/apimachinery v0.22.4
sigs.k8s.io/kind v0.11.1
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2124,8 +2124,9 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8-0.20211015140901-98f6e0395b11 h1:tH6DicuZbyfFY5UhxRxZP2ksWtFE8XLpoJX+YFzBIUI=
golang.org/x/tools v0.1.8-0.20211015140901-98f6e0395b11/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
157 changes: 157 additions & 0 deletions internal/sbom/mod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2021 Google LLC All Rights Reserved.
//
// 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.

// TODO: All of this is copied from:
// https://cs.opensource.google/go/go/+/master:src/debug/buildinfo/buildinfo.go
// https://cs.opensource.google/go/go/+/master:src/runtime/debug/mod.go
// It should be replaced with runtime/buildinfo.Read on the binary file when Go 1.18 is released.

package sbom

import (
"bytes"
"fmt"
)

// BuildInfo represents the build information read from a Go binary.
type BuildInfo struct {
GoVersion string // Version of Go that produced this binary.
Path string // The main package path
Main Module // The module containing the main package
Deps []*Module // Module dependencies
Settings []BuildSetting // Other information about the build.
}

// Module represents a module.
type Module struct {
Path string // module path
Version string // module version
Sum string // checksum
Replace *Module // replaced by this module
}

// BuildSetting describes a setting that may be used to understand how the
// binary was built. For example, VCS commit and dirty status is stored here.
type BuildSetting struct {
// Key and Value describe the build setting. They must not contain tabs
// or newlines.
Key, Value string
}

func (bi *BuildInfo) UnmarshalText(data []byte) (err error) {
*bi = BuildInfo{}
lineNum := 1
defer func() {
if err != nil {
err = fmt.Errorf("could not parse Go build info: line %d: %w", lineNum, err)
}
}()

var (
pathLine = []byte("path\t")
modLine = []byte("mod\t")
depLine = []byte("dep\t")
repLine = []byte("=>\t")
buildLine = []byte("build\t")
newline = []byte("\n")
tab = []byte("\t")
)

readModuleLine := func(elem [][]byte) (Module, error) {
if len(elem) != 2 && len(elem) != 3 {
return Module{}, fmt.Errorf("expected 2 or 3 columns; got %d", len(elem))
}
sum := ""
if len(elem) == 3 {
sum = string(elem[2])
}
return Module{
Path: string(elem[0]),
Version: string(elem[1]),
Sum: sum,
}, nil
}

var (
last *Module
line []byte
ok bool
)
// Reverse of BuildInfo.String(), except for go version.
for len(data) > 0 {
line, data, ok = bytesCut(data, newline)
if !ok {
break
}
line = bytes.TrimPrefix(line, []byte("\t"))
switch {
case bytes.HasPrefix(line, pathLine):
elem := line[len(pathLine):]
bi.Path = string(elem)
case bytes.HasPrefix(line, modLine):
elem := bytes.Split(line[len(modLine):], tab)
last = &bi.Main
*last, err = readModuleLine(elem)
if err != nil {
return err
}
case bytes.HasPrefix(line, depLine):
elem := bytes.Split(line[len(depLine):], tab)
last = new(Module)
bi.Deps = append(bi.Deps, last)
*last, err = readModuleLine(elem)
if err != nil {
return err
}
case bytes.HasPrefix(line, repLine):
elem := bytes.Split(line[len(repLine):], tab)
if len(elem) != 3 {
return fmt.Errorf("expected 3 columns for replacement; got %d", len(elem))
}
if last == nil {
return fmt.Errorf("replacement with no module on previous line")
}
last.Replace = &Module{
Path: string(elem[0]),
Version: string(elem[1]),
Sum: string(elem[2]),
}
last = nil
case bytes.HasPrefix(line, buildLine):
elem := bytes.Split(line[len(buildLine):], tab)
if len(elem) != 2 {
return fmt.Errorf("expected 2 columns for build setting; got %d", len(elem))
}
if len(elem[0]) == 0 {
return fmt.Errorf("empty key")
}
bi.Settings = append(bi.Settings, BuildSetting{Key: string(elem[0]), Value: string(elem[1])})
}
lineNum++
}
return nil
}

// bytesCut slices s around the first instance of sep,
// returning the text before and after sep.
// The found result reports whether sep appears in s.
// If sep does not appear in s, cut returns s, nil, false.
//
// bytesCut returns slices of the original slice s, not copies.
func bytesCut(s, sep []byte) (before, after []byte, found bool) {
if i := bytes.Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, nil, false
}
94 changes: 94 additions & 0 deletions internal/sbom/spdx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2021 Google LLC All Rights Reserved.
//
// 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 sbom

import (
"bytes"
"strings"
"text/template"
"time"
)

const dateFormat = "2006-01-02T15:04:05Z"

func GenerateSPDX(koVersion string, date time.Time, mod []byte) ([]byte, error) {
bi := &BuildInfo{}
if err := bi.UnmarshalText(mod); err != nil {
return nil, err
}

var buf bytes.Buffer
if err := tmpl.Execute(&buf, tmplInfo{
BuildInfo: *bi,
Date: date.Format(dateFormat),
KoVersion: koVersion,
}); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

type tmplInfo struct {
BuildInfo
Date, UUID, KoVersion string
}

// TODO: use k8s.io/release/pkg/bom
var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
"dots": func(s string) string { return strings.ReplaceAll(s, "/", ".") },
}).Parse(`SPDXVersion: SPDX-2.2
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: {{ .BuildInfo.Main.Path }}
DocumentNamespace: http://spdx.org/spdxpackages/{{ .BuildInfo.Main.Path }}
Creator: Tool: ko {{ .KoVersion }}
Created: {{ .Date }}
##### Package representing {{ .BuildInfo.Main.Path }}
PackageName: {{ .BuildInfo.Main.Path }}
SPDXID: SPDXRef-Package-{{ .BuildInfo.Main.Path | dots }}
PackageSupplier: Organization: {{ .BuildInfo.Main.Path }}
PackageDownloadLocation: https://{{ .BuildInfo.Main.Path }}
FilesAnalyzed: false
PackageHomePage: https://{{ .BuildInfo.Main.Path }}
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
PackageLicenseComments: NOASSERTION
PackageComment: NOASSERTION
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-{{ .BuildInfo.Main.Path | dots }}
{{ range .Deps }}
Relationship: SPDXRef-Package-{{ $.Main.Path | dots }} DEPENDS_ON SPDXRef-Package-{{ .Path | dots }}-{{ .Version }}{{ end }}
{{ range .Deps }}
##### Package representing {{ .Path }}
PackageName: {{ .Path }}
SPDXID: SPDXRef-Package-{{ .Path | dots }}-{{ .Version }}
PackageVersion: {{ .Version }}
PackageSupplier: Organization: {{ .Path }}
PackageDownloadLocation: https://proxy.golang.org/{{ .Path }}/@v/{{ .Version }}.zip
FilesAnalyzed: false
PackageChecksum: SHA256: {{ .Sum }}
PackageLicenseConcluded: NOASSERTION
PackageLicenseDeclared: NOASSERTION
PackageCopyrightText: NOASSERTION
PackageLicenseComments: NOASSERTION
PackageComment: NOASSERTION
{{ end }}
`))
36 changes: 34 additions & 2 deletions pkg/commands/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,27 @@ package commands

import (
"archive/tar"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/ko/internal/sbom"
"github.com/spf13/cobra"
)

// addDeps augments our CLI surface with deps.
func addDeps(topLevel *cobra.Command) {
var sbomType string
deps := &cobra.Command{
Use: "deps IMAGE",
Short: "Print Go module dependency information about the ko-built binary in the image",
Expand All @@ -46,6 +50,12 @@ If the image was not built using ko, or if it was built without embedding depend
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

switch sbomType {
case "spdx", "go.version-m":
default:
return fmt.Errorf("invalid sbom type %q: must be spdx or go.version-m", sbomType)
}

ref, err := name.ParseReference(args[0])
if err != nil {
return err
Expand Down Expand Up @@ -110,12 +120,34 @@ If the image was not built using ko, or if it was built without embedding depend
return err
}
cmd := exec.CommandContext(ctx, "go", "version", "-m", n)
cmd.Stdout = os.Stdout
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = os.Stderr
return cmd.Run()
if err := cmd.Run(); err != nil {
return err
}
// In order to get deterministics SBOMs replace
// our randomized file name with the path the
// app will get inside of the container.
mod := bytes.Replace(buf.Bytes(),
[]byte(n),
[]byte(path.Join("/ko-app", filepath.Base(filepath.Clean(h.Name)))),
1)
switch sbomType {
case "spdx":
b, err := sbom.GenerateSPDX(Version, cfg.Created.Time, mod)
if err != nil {
return err
}
io.Copy(os.Stdout, bytes.NewReader(b))
case "go.version-m":
io.Copy(os.Stdout, bytes.NewReader(mod))
}
return nil
}
// unreachable
},
}
deps.Flags().StringVar(&sbomType, "sbom", "go.version-m", "Format for SBOM output")
topLevel.AddCommand(deps)
}
Loading

0 comments on commit af2ff52

Please sign in to comment.