Skip to content

Commit

Permalink
Start emitting multi-arch SBOMs for SPDX with ko (ko-build#743)
Browse files Browse the repository at this point in the history
This plumbs through support for building multi-arch SPDX SBOMs largely based on Puerco's outline, but with a few
adaptations.  I added a few minor refactorings to try to enable consistency across the Image/Index SBOMs.

Related: ko-build#655
  • Loading branch information
mattmoor authored Jul 5, 2022
1 parent 787d625 commit 2299765
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 116 deletions.
57 changes: 49 additions & 8 deletions .github/workflows/sbom.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ jobs:
- 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
go run ./ deps $img --sbom=cyclonedx > cyclonedx.json
./cyclonedx-linux-x64 validate --input-file=cyclonedx.json --fail-on-errors
- uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: sbom.json
path: sbom.json
name: cyclonedx.json
path: cyclonedx.json

spdx:
name: Validate SPDX SBOM
Expand Down Expand Up @@ -90,12 +90,53 @@ jobs:
- name: Generate and Validate
run: |
img=$(go run ./ build ./)
go run ./ deps $img --sbom=spdx | tee sbom.json
go run ./ deps $img --sbom=spdx | tee spdx.json
java -jar ./tools-java-1.0.4-jar-with-dependencies.jar Verify sbom.json
java -jar ./tools-java-1.0.4-jar-with-dependencies.jar Verify spdx.json
- uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: sbom.json
path: sbom.json
name: spdx.json
path: spdx.json

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

env:
KO_DOCKER_REPO: localhost:1338

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

- name: Install SPDX Tools
run: |
wget https://github.com/spdx/tools-java/releases/download/v1.0.4/tools-java-1.0.4.zip
unzip tools-java-1.0.4.zip
- name: Install Cosign
uses: sigstore/[email protected]
with:
cosign-release: 'v1.7.2'

- name: Generate and Validate
run: |
img=$(go run ./ build --platform=linux/amd64,linux/arm64 ./)
cosign download sbom $img | tee spdx-multi-arch.json
java -jar ./tools-java-1.0.4-jar-with-dependencies.jar Verify spdx-multi-arch.json
- uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: spdx-multi-arch.json
path: spdx-multi-arch.json
51 changes: 15 additions & 36 deletions internal/sbom/cyclonedx.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,11 @@ import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strings"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/sigstore/cosign/pkg/oci"
)

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

func goRef(path, version string) string {
// Try to lowercase the first 2 path elements to comply with spec
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#golang
p := strings.Split(path, "/")
if len(p) > 2 {
path = strings.Join(
append(
[]string{strings.ToLower(p[0]), strings.ToLower(p[1])},
p[2:]...,
), "/",
)
}
return fmt.Sprintf("pkg:golang/%s@%s?type=module", path, version)
}

func ociRef(path string, imgDigest v1.Hash) string {
parts := strings.Split(path, "/")
return fmt.Sprintf("pkg:oci/%s@%s", parts[len(parts)-1], imgDigest.String())
}

func h1ToSHA256(s string) string {
if !strings.HasPrefix(s, "h1:") {
return ""
Expand All @@ -60,7 +35,7 @@ func h1ToSHA256(s string) string {
return hex.EncodeToString(b)
}

func GenerateCycloneDX(mod []byte) ([]byte, error) {
func GenerateImageCycloneDX(mod []byte) ([]byte, error) {
var err error
mod, err = massageGoVersionM(mod)
if err != nil {
Expand All @@ -78,11 +53,11 @@ func GenerateCycloneDX(mod []byte) ([]byte, error) {
Version: 1,
Metadata: metadata{
Component: component{
BOMRef: bomRef(bi.Main.Path, bi.Main.Version),
BOMRef: bomRef(&bi.Main),
Type: "application",
Name: bi.Main.Path,
Version: bi.Main.Version,
Purl: bomRef(bi.Main.Path, bi.Main.Version),
Purl: bomRef(&bi.Main),
ExternalReferences: []externalReference{{
URL: "https://" + bi.Main.Path,
Type: "vcs",
Expand All @@ -97,12 +72,12 @@ func GenerateCycloneDX(mod []byte) ([]byte, error) {
// TODO: include bi.Settings?
},
Dependencies: []dependency{{
Ref: bomRef(bi.Main.Path, bi.Main.Version),
Ref: bomRef(&bi.Main),
}},
Compositions: []composition{{
Aggregate: "complete",
Dependencies: []string{
bomRef(bi.Main.Path, bi.Main.Version),
bomRef(&bi.Main),
},
}, {
Aggregate: "unknown",
Expand All @@ -115,12 +90,12 @@ func GenerateCycloneDX(mod []byte) ([]byte, error) {
continue
}
comp := component{
BOMRef: bomRef(dep.Path, dep.Version),
BOMRef: bomRef(dep),
Type: "library",
Name: dep.Path,
Version: dep.Version,
Scope: "required",
Purl: bomRef(dep.Path, dep.Version),
Purl: bomRef(dep),
ExternalReferences: []externalReference{{
URL: "https://" + dep.Path,
Type: "vcs",
Expand All @@ -133,12 +108,12 @@ func GenerateCycloneDX(mod []byte) ([]byte, error) {
}}
}
doc.Components = append(doc.Components, comp)
doc.Dependencies[0].DependsOn = append(doc.Dependencies[0].DependsOn, bomRef(dep.Path, dep.Version))
doc.Dependencies[0].DependsOn = append(doc.Dependencies[0].DependsOn, bomRef(dep))
doc.Dependencies = append(doc.Dependencies, dependency{
Ref: bomRef(dep.Path, dep.Version),
Ref: bomRef(dep),
})

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

var buf bytes.Buffer
Expand All @@ -150,6 +125,10 @@ func GenerateCycloneDX(mod []byte) ([]byte, error) {
return buf.Bytes(), nil
}

func GenerateIndexCycloneDX(sii oci.SignedImageIndex) ([]byte, error) {
return nil, nil
}

type document struct {
BOMFormat string `json:"bomFormat"`
SpecVersion string `json:"specVersion"`
Expand Down
28 changes: 27 additions & 1 deletion internal/sbom/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// TODO: All of this is copied from:
// TODO: Most 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.
Expand All @@ -28,6 +28,32 @@ import (
"strings"
)

func modulePackageName(mod *Module) string {
return fmt.Sprintf("SPDXRef-Package-%s-%s",
strings.ReplaceAll(mod.Path, "/", "."),
mod.Version)
}

func bomRef(mod *Module) string {
return fmt.Sprintf("pkg:golang/%s@%s?type=module", mod.Path, mod.Version)
}

func goRef(mod *Module) string {
path := mod.Path
// Try to lowercase the first 2 path elements to comply with spec
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#golang
p := strings.Split(path, "/")
if len(p) > 2 {
path = strings.Join(
append(
[]string{strings.ToLower(p[0]), strings.ToLower(p[1])},
p[2:]...,
), "/",
)
}
return fmt.Sprintf("pkg:golang/%s@%s?type=module", path, mod.Version)
}

// BuildInfo represents the build information read from a Go binary.
// https://cs.opensource.google/go/go/+/release-branch.go1.18:src/runtime/debug/mod.go;drc=release-branch.go1.18;l=41
type BuildInfo struct {
Expand Down
27 changes: 27 additions & 0 deletions internal/sbom/mod_1.18.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package sbom
import (
"fmt"
"runtime/debug"
"strings"
)

type BuildInfo debug.BuildInfo
Expand All @@ -32,3 +33,29 @@ func ParseBuildInfo(data string) (*BuildInfo, error) {
bi := BuildInfo(*dbi)
return &bi, nil
}

func modulePackageName(mod *debug.Module) string {
return fmt.Sprintf("SPDXRef-Package-%s-%s",
strings.ReplaceAll(mod.Path, "/", "."),
mod.Version)
}

func bomRef(mod *debug.Module) string {
return fmt.Sprintf("pkg:golang/%s@%s?type=module", mod.Path, mod.Version)
}

func goRef(mod *debug.Module) string {
path := mod.Path
// Try to lowercase the first 2 path elements to comply with spec
// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#golang
p := strings.Split(path, "/")
if len(p) > 2 {
path = strings.Join(
append(
[]string{strings.ToLower(p[0]), strings.ToLower(p[1])},
p[2:]...,
), "/",
)
}
return fmt.Sprintf("pkg:golang/%s@%s?type=module", path, mod.Version)
}
Loading

0 comments on commit 2299765

Please sign in to comment.