Skip to content

Commit

Permalink
Generate CycloneDX SBOMs using our own JSON generation (#587)
Browse files Browse the repository at this point in the history
* Generate CycloneDX SBOMs using our own JSON generation

* fix some errors

* Add support to ko deps

* Add e2e SBOM validation

* ignore empty hashes (why are hashes empty?)
  • Loading branch information
imjasonh committed Feb 11, 2022
1 parent f8d0aca commit 98ff104
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 3 deletions.
98 changes: 98 additions & 0 deletions .github/workflows/sbom.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Validate SBOMs

on:
pull_request:
branches: ['main']

jobs:
go-version-m:
name: Generate go version -m
runs-on: ubuntu-latest

env:
KO_DOCKER_REPO: localhost:1338

steps:
- uses: actions/setup-go@v2
with:
go-version: '1.17.x'
- name: Install cmd/registry
run: |
go install github.com/google/go-containerregistry/cmd/registry@latest
registry &
- uses: actions/checkout@v2

- name: Generate
run: |
img=$(go run ./ build ./)
go run ./ deps $img --sbom=go.version-m > gomod.txt
cat gomod.txt
cyclonedx:
name: Validate CycloneDX SBOM
runs-on: ubuntu-latest

env:
KO_DOCKER_REPO: localhost:1338

steps:
- uses: actions/setup-go@v2
with:
go-version: '1.17.x'
- name: Install cmd/registry
run: |
go install github.com/google/go-containerregistry/cmd/registry@latest
registry &
- uses: actions/checkout@v2

- name: Install CycloneDX
run: |
wget https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.22.0/cyclonedx-linux-x64
chmod +x cyclonedx-linux-x64
- name: Generate and Validate
run: |
img=$(go run ./ build ./)
go run ./ deps $img --sbom=cyclonedx > sbom.json
./cyclonedx-linux-x64 validate --input-file=sbom.json --fail-on-errors
- uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: sbom.json
path: sbom.json

spdx:
name: Validate SPDX SBOM
runs-on: ubuntu-latest

env:
KO_DOCKER_REPO: localhost:1338

steps:
- uses: actions/setup-go@v2
with:
go-version: '1.17.x'
- name: Install cmd/registry
run: |
go install github.com/google/go-containerregistry/cmd/registry@latest
registry &
- uses: actions/checkout@v2

- name: Install SPDX Tools
run: |
wget https://github.com/spdx/tools/releases/download/v2.2.7/spdx-tools-2.2.7.zip
unzip spdx-tools-2.2.7.zip
- name: Generate and Validate
run: |
img=$(go run ./ build ./)
go run ./ deps $img --sbom=spdx > sbom.txt
java -jar ./spdx-tools-2.2.7-jar-with-dependencies.jar Verify sbom.txt
- uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: sbom.txt
path: sbom.txt
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.10.1
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/tools v0.1.9
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
Expand Down
4 changes: 3 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1987,8 +1987,9 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -2114,6 +2115,7 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127074510-2fabfed7e28f h1:o66Bv9+w/vuk7Krcig9jZqD01FP7BL8OliFqqw0xzPI=
golang.org/x/net v0.0.0-20220127074510-2fabfed7e28f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
Expand Down
167 changes: 167 additions & 0 deletions internal/sbom/cyclonedx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2022 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"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
)

func bomRef(path, version string) string {
return fmt.Sprintf("pkg:golang/%s@%s?type=module", path, version)
}

func h1ToSHA256(s string) string {
if !strings.HasPrefix(s, "h1:") {
return ""
}
b, err := base64.StdEncoding.DecodeString(s[3:])
if err != nil {
return ""
}
return hex.EncodeToString(b)
}

func GenerateCycloneDX(mod []byte) ([]byte, error) {
bi := &BuildInfo{}
if err := bi.UnmarshalText(mod); err != nil {
return nil, err
}

doc := document{
BOMFormat: "CycloneDX",
SpecVersion: "1.4",
Version: 1,
Metadata: metadata{
Component: component{
BOMRef: bomRef(bi.Main.Path, bi.Main.Version),
Type: "application",
Name: bi.Main.Path,
Version: bi.Main.Version,
Purl: bomRef(bi.Main.Path, bi.Main.Version),
ExternalReferences: []externalReference{{
URL: "https://" + bi.Main.Path,
Type: "vcs",
}},
},
Properties: []property{{
Name: "cdx:gomod:binary:name",
Value: "out",
}},
// TODO: include all hashes
// TODO: include go version
// TODO: include bi.Settings?
},
Dependencies: []dependency{{
Ref: bomRef(bi.Main.Path, bi.Main.Version),
}},
Compositions: []composition{{
Aggregate: "complete",
Dependencies: []string{
bomRef(bi.Main.Path, bi.Main.Version),
},
}, {
Aggregate: "unknown",
Dependencies: []string{},
}},
}
for _, dep := range bi.Deps {
// Don't include replaced deps
if dep.Replace != nil {
continue
}
comp := component{
BOMRef: bomRef(dep.Path, dep.Version),
Type: "library",
Name: dep.Path,
Version: dep.Version,
Scope: "required",
Purl: bomRef(dep.Path, dep.Version),
ExternalReferences: []externalReference{{
URL: "https://" + dep.Path,
Type: "vcs",
}},
}
if dep.Sum != "" {
comp.Hashes = []hash{{
Alg: "SHA-256",
Content: h1ToSHA256(dep.Sum),
}}
}
doc.Components = append(doc.Components, comp)
doc.Dependencies[0].DependsOn = append(doc.Dependencies[0].DependsOn, bomRef(dep.Path, dep.Version))
doc.Dependencies = append(doc.Dependencies, dependency{
Ref: bomRef(dep.Path, dep.Version),
})

doc.Compositions[1].Dependencies = append(doc.Compositions[1].Dependencies, bomRef(dep.Path, dep.Version))
}

var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ")
if err := enc.Encode(doc); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

type document struct {
BOMFormat string `json:"bomFormat"`
SpecVersion string `json:"specVersion"`
Version int `json:"version"`
Metadata metadata `json:"metadata"`
Components []component `json:"components,omitempty"`
Dependencies []dependency `json:"dependencies,omitempty"`
Compositions []composition `json:"compositions,omitempty"`
}
type metadata struct {
Component component `json:"component"`
Properties []property `json:"properties,omitempty"`
}
type component struct {
BOMRef string `json:"bom-ref"`
Type string `json:"type"`
Name string `json:"name"`
Version string `json:"version"`
Scope string `json:"scope,omitempty"`
Hashes []hash `json:"hashes,omitempty"`
Purl string `json:"purl"`
ExternalReferences []externalReference `json:"externalReferences"`
}
type hash struct {
Alg string `json:"alg"`
Content string `json:"content"`
}
type externalReference struct {
URL string `json:"url"`
Type string `json:"type"`
}
type property struct {
Name string `json:"name"`
Value string `json:"value"`
}
type dependency struct {
Ref string `json:"ref"`
DependsOn []string `json:"dependsOn,omitempty"`
}
type composition struct {
Aggregate string `json:"aggregate"`
Dependencies []string `json:"dependencies,omitempty"`
}
15 changes: 15 additions & 0 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,21 @@ func spdx(version string) sbomber {
}
}

func cycloneDX() 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
}

b, err = sbom.GenerateCycloneDX(b)
if err != nil {
return nil, "", err
}
return b, ctypes.CycloneDXMediaType, 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
9 changes: 9 additions & 0 deletions pkg/build/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ func WithSPDX(version string) Option {
}
}

// WithCycloneDX is a functional option to direct ko to use CycloneDX for SBOM
// format.
func WithCycloneDX() Option {
return func(gbo *gobuildOpener) error {
gbo.sbom = cycloneDX()
return nil
}
}

// withSBOMber is a functional option for overriding the way SBOMs
// are generated.
func withSBOMber(sbom sbomber) Option {
Expand Down
8 changes: 7 additions & 1 deletion pkg/commands/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ If the image was not built using ko, or if it was built without embedding depend
ctx := cmd.Context()

switch sbomType {
case "spdx", "go.version-m":
case "cyclonedx", "spdx", "go.version-m":
default:
return fmt.Errorf("invalid sbom type %q: must be spdx or go.version-m", sbomType)
}
Expand Down Expand Up @@ -139,6 +139,12 @@ If the image was not built using ko, or if it was built without embedding depend
return err
}
io.Copy(os.Stdout, bytes.NewReader(b))
case "cyclonedx":
b, err := sbom.GenerateCycloneDX(mod)
if err != nil {
return err
}
io.Copy(os.Stdout, bytes.NewReader(b))
case "go.version-m":
io.Copy(os.Stdout, bytes.NewReader(mod))
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/commands/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) {
opts = append(opts, build.WithDisabledSBOM())
case "go.version-m":
opts = append(opts, build.WithGoVersionSBOM())
case "cyclonedx":
opts = append(opts, build.WithCycloneDX())
default: // "spdx"
opts = append(opts, build.WithSPDX(version()))
}
Expand Down
3 changes: 2 additions & 1 deletion vendor/modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,8 @@ github.com/subosito/gotenv
github.com/theupdateframework/go-tuf/encrypted
# github.com/vbatts/tar-split v0.11.2
github.com/vbatts/tar-split/archive/tar
# golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
# golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
## explicit
golang.org/x/crypto/internal/poly1305
golang.org/x/crypto/internal/subtle
golang.org/x/crypto/nacl/secretbox
Expand Down

0 comments on commit 98ff104

Please sign in to comment.