From 2299765c54b3ba0399c9ed0d57bf310b5c0f8cb8 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Tue, 5 Jul 2022 12:47:15 -0700 Subject: [PATCH] Start emitting multi-arch SBOMs for `SPDX` with `ko` (#743) 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: https://github.com/google/ko/issues/655 --- .github/workflows/sbom.yaml | 57 +++++++-- internal/sbom/cyclonedx.go | 51 +++----- internal/sbom/mod.go | 28 ++++- internal/sbom/mod_1.18.go | 27 +++++ internal/sbom/spdx.go | 234 ++++++++++++++++++++++++++++++++---- pkg/build/gobuild.go | 119 +++++++++++------- pkg/build/gobuild_test.go | 4 +- pkg/commands/deps.go | 9 +- 8 files changed, 413 insertions(+), 116 deletions(-) diff --git a/.github/workflows/sbom.yaml b/.github/workflows/sbom.yaml index acd15badbf..c334ea8064 100644 --- a/.github/workflows/sbom.yaml +++ b/.github/workflows/sbom.yaml @@ -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 @@ -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/cosign-installer@v2.4.0 + 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 diff --git a/internal/sbom/cyclonedx.go b/internal/sbom/cyclonedx.go index 572b1bdc32..bb3132c390 100644 --- a/internal/sbom/cyclonedx.go +++ b/internal/sbom/cyclonedx.go @@ -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 "" @@ -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 { @@ -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", @@ -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", @@ -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", @@ -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 @@ -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"` diff --git a/internal/sbom/mod.go b/internal/sbom/mod.go index c3925d18aa..2da560a81a 100644 --- a/internal/sbom/mod.go +++ b/internal/sbom/mod.go @@ -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. @@ -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 { diff --git a/internal/sbom/mod_1.18.go b/internal/sbom/mod_1.18.go index d8bf2b4a54..6857476540 100644 --- a/internal/sbom/mod_1.18.go +++ b/internal/sbom/mod_1.18.go @@ -20,6 +20,7 @@ package sbom import ( "fmt" "runtime/debug" + "strings" ) type BuildInfo debug.BuildInfo @@ -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) +} diff --git a/internal/sbom/spdx.go b/internal/sbom/spdx.go index 9b5649fb20..cd69a56861 100644 --- a/internal/sbom/spdx.go +++ b/internal/sbom/spdx.go @@ -17,16 +17,40 @@ package sbom import ( "bytes" "encoding/json" + "errors" "fmt" + "net/url" "strings" "time" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/sigstore/cosign/pkg/oci" ) +type qualifier struct { + key string + value string +} + +// ociRef constructs a pURL for the OCI image according to: +// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#oci +func ociRef(path string, imgDigest v1.Hash, qual ...qualifier) string { + parts := strings.Split(path, "/") + purl := fmt.Sprintf("pkg:oci/%s@%s", parts[len(parts)-1], imgDigest.String()) + if num := len(qual); num > 0 { + qs := make(url.Values, num) + for _, q := range qual { + qs.Add(q.key, q.value) + } + purl = purl + "?" + qs.Encode() + } + return purl +} + const dateFormat = "2006-01-02T15:04:05Z" -func GenerateSPDX(koVersion string, date time.Time, mod []byte, imgDigest v1.Hash) ([]byte, error) { +func GenerateImageSPDX(koVersion string, mod []byte, img oci.SignedImage) ([]byte, error) { var err error mod, err = massageGoVersionM(mod) if err != nil { @@ -38,26 +62,49 @@ func GenerateSPDX(koVersion string, date time.Time, mod []byte, imgDigest v1.Has return nil, err } - mainPackageID := "SPDXRef-Package-" + strings.ReplaceAll(bi.Main.Path, "/", ".") - - doc := Document{ - Version: Version, - DataLicense: "CC0-1.0", - ID: "SPDXRef-DOCUMENT", - Name: bi.Main.Path, - Namespace: "http://spdx.org/spdxdocs/" + bi.Main.Path, - DocumentDescribes: []string{mainPackageID}, - CreationInfo: CreationInfo{ - Created: date.Format(dateFormat), - Creators: []string{"Tool: ko " + koVersion}, - }, - Packages: make([]Package, 0, 1+len(bi.Deps)), - Relationships: make([]Relationship, 0, 1+len(bi.Deps)), + imgDigest, err := img.Digest() + if err != nil { + return nil, err + } + cfg, err := img.ConfigFile() + if err != nil { + return nil, err } + doc, imageID := starterDocument(koVersion, cfg.Created.Time, imgDigest) + + // image -> main package -> transitive deps + doc.Packages = make([]Package, 0, 2+len(bi.Deps)) + doc.Relationships = make([]Relationship, 0, 2+len(bi.Deps)) + doc.Relationships = append(doc.Relationships, Relationship{ Element: "SPDXRef-DOCUMENT", Type: "DESCRIBES", + Related: imageID, + }) + + doc.Packages = append(doc.Packages, Package{ + ID: imageID, + Name: imgDigest.String(), + // TODO: PackageSupplier: "Organization: " + bs.Main.Path + DownloadLocation: NOASSERTION, + FilesAnalyzed: false, + // TODO: PackageHomePage: "https://" + bi.Main.Path, + LicenseConcluded: NOASSERTION, + LicenseDeclared: NOASSERTION, + CopyrightText: NOASSERTION, + ExternalRefs: []ExternalRef{{ + Category: "PACKAGE_MANAGER", + Type: "purl", + Locator: ociRef("image", imgDigest), + }}, + }) + + mainPackageID := modulePackageName(&bi.Main) + + doc.Relationships = append(doc.Relationships, Relationship{ + Element: imageID, + Type: "CONTAINS", Related: mainPackageID, }) @@ -74,14 +121,12 @@ func GenerateSPDX(koVersion string, date time.Time, mod []byte, imgDigest v1.Has ExternalRefs: []ExternalRef{{ Category: "PACKAGE_MANAGER", Type: "purl", - Locator: ociRef(bi.Path, imgDigest), + Locator: goRef(&bi.Main), }}, }) for _, dep := range bi.Deps { - depID := fmt.Sprintf("SPDXRef-Package-%s-%s", - strings.ReplaceAll(dep.Path, "/", "."), - dep.Version) + depID := modulePackageName(dep) doc.Relationships = append(doc.Relationships, Relationship{ Element: mainPackageID, @@ -90,8 +135,8 @@ func GenerateSPDX(koVersion string, date time.Time, mod []byte, imgDigest v1.Has }) pkg := Package{ - Name: dep.Path, ID: depID, + Name: dep.Path, Version: dep.Version, // TODO: PackageSupplier: "Organization: " + dep.Path DownloadLocation: fmt.Sprintf("https://proxy.golang.org/%s/@v/%s.zip", dep.Path, dep.Version), @@ -102,7 +147,7 @@ func GenerateSPDX(koVersion string, date time.Time, mod []byte, imgDigest v1.Has ExternalRefs: []ExternalRef{{ Category: "PACKAGE_MANAGER", Type: "purl", - Locator: goRef(dep.Path, dep.Version), + Locator: goRef(dep), }}, } @@ -125,6 +170,151 @@ func GenerateSPDX(koVersion string, date time.Time, mod []byte, imgDigest v1.Has return buf.Bytes(), nil } +func extractDate(sii oci.SignedImageIndex) (*time.Time, error) { + im, err := sii.IndexManifest() + if err != nil { + return nil, err + } + for _, desc := range im.Manifests { + switch desc.MediaType { + case types.OCIManifestSchema1, types.DockerManifestSchema2: + si, err := sii.SignedImage(desc.Digest) + if err != nil { + return nil, err + } + cfg, err := si.ConfigFile() + if err != nil { + return nil, err + } + return &cfg.Created.Time, nil + + default: + // We shouldn't need to handle nested indices, since we don't build + // them, but if we do we will need to do some sort of recursion here. + return nil, fmt.Errorf("unknown media type: %v", desc.MediaType) + } + } + return nil, errors.New("unable to extract date, no imaged found") +} + +func GenerateIndexSPDX(koVersion string, sii oci.SignedImageIndex) ([]byte, error) { + indexDigest, err := sii.Digest() + if err != nil { + return nil, err + } + + date, err := extractDate(sii) + if err != nil { + return nil, err + } + + doc, indexID := starterDocument(koVersion, *date, indexDigest) + doc.Packages = []Package{{ + ID: indexID, + Name: indexDigest.String(), + DownloadLocation: NOASSERTION, + FilesAnalyzed: false, + LicenseConcluded: NOASSERTION, + LicenseDeclared: NOASSERTION, + CopyrightText: NOASSERTION, + Checksums: []Checksum{{ + Algorithm: strings.ToUpper(indexDigest.Algorithm), + Value: indexDigest.Hex, + }}, + ExternalRefs: []ExternalRef{{ + Category: "PACKAGE_MANAGER", + Type: "purl", + Locator: ociRef("index", indexDigest), + }}, + }} + + im, err := sii.IndexManifest() + if err != nil { + return nil, err + } + for _, desc := range im.Manifests { + switch desc.MediaType { + case types.OCIManifestSchema1, types.DockerManifestSchema2: + si, err := sii.SignedImage(desc.Digest) + if err != nil { + return nil, err + } + + imageDigest, err := si.Digest() + if err != nil { + return nil, err + } + + depID := ociPackageName(imageDigest) + + doc.Relationships = append(doc.Relationships, Relationship{ + Element: ociPackageName(indexDigest), + Type: "CONTAINS", + Related: depID, + }) + + pkg := Package{ + ID: depID, + Name: imageDigest.String(), + Version: desc.Platform.String(), + // TODO: PackageSupplier: "Organization: " + dep.Path + DownloadLocation: NOASSERTION, + FilesAnalyzed: false, + LicenseConcluded: NOASSERTION, + LicenseDeclared: NOASSERTION, + CopyrightText: NOASSERTION, + ExternalRefs: []ExternalRef{{ + Category: "PACKAGE_MANAGER", + Type: "purl", + Locator: ociRef("image", imageDigest, qualifier{ + key: "arch", + value: desc.Platform.Architecture, + }), + }}, + Checksums: []Checksum{{ + Algorithm: strings.ToUpper(imageDigest.Algorithm), + Value: imageDigest.Hex, + }}, + } + + doc.Packages = append(doc.Packages, pkg) + + default: + // We shouldn't need to handle nested indices, since we don't build + // them, but if we do we will need to do some sort of recursion here. + return nil, fmt.Errorf("unknown media type: %v", desc.MediaType) + } + } + + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetIndent("", " ") + if err := enc.Encode(doc); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func ociPackageName(d v1.Hash) string { + return fmt.Sprintf("SPDXRef-Package-%s-%s", d.Algorithm, d.Hex) +} + +func starterDocument(koVersion string, date time.Time, d v1.Hash) (Document, string) { + digestID := ociPackageName(d) + return Document{ + ID: "SPDXRef-DOCUMENT", + Version: Version, + CreationInfo: CreationInfo{ + Created: date.Format(dateFormat), + Creators: []string{"Tool: ko " + koVersion}, + }, + DataLicense: "CC0-1.0", + Name: "sbom-" + d.String(), + Namespace: "http://spdx.org/spdxdocs/ko" + d.String(), + DocumentDescribes: []string{digestID}, + }, digestID +} + // Below this is forked from here: // https://github.com/kubernetes-sigs/bom/blob/main/pkg/spdx/json/v2.2.2/types.go diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index f220d4eef6..a5c0f02eb6 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -62,7 +62,8 @@ 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, v1.Image) ([]byte, types.MediaType, error) + +type sbomber func(context.Context, string, string, oci.SignedEntity) ([]byte, types.MediaType, error) type platformMatcher struct { spec []string @@ -300,55 +301,76 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con return file, nil } -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 - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return nil, "", err - } +func goversionm(ctx context.Context, file string, appPath string, se oci.SignedEntity) ([]byte, types.MediaType, error) { + switch se.(type) { + case oci.SignedImage: + 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 + } + + // 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 - // 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 + case oci.SignedImageIndex: + return nil, "", nil + + default: + return nil, "", fmt.Errorf("unrecognized type: %T", se) + } } 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 - } + return func(ctx context.Context, file string, appPath string, se oci.SignedEntity) ([]byte, types.MediaType, error) { + switch obj := se.(type) { + case oci.SignedImage: + b, _, err := goversionm(ctx, file, appPath, obj) + if err != nil { + return nil, "", err + } - cfg, err := img.ConfigFile() - if err != nil { - return nil, "", err - } - imgDigest, err := img.Digest() - if err != nil { - return nil, "", err - } - b, err = sbom.GenerateSPDX(version, cfg.Created.Time, b, imgDigest) - if err != nil { - return nil, "", err + b, err = sbom.GenerateImageSPDX(version, b, obj) + if err != nil { + return nil, "", err + } + return b, ctypes.SPDXJSONMediaType, nil + + case oci.SignedImageIndex: + b, err := sbom.GenerateIndexSPDX(version, obj) + return b, ctypes.SPDXJSONMediaType, err + + default: + return nil, "", fmt.Errorf("unrecognized type: %T", se) } - return b, ctypes.SPDXJSONMediaType, nil } } 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 - } + return func(ctx context.Context, file string, appPath string, se oci.SignedEntity) ([]byte, types.MediaType, error) { + switch obj := se.(type) { + case oci.SignedImage: + b, _, err := goversionm(ctx, file, appPath, obj) + if err != nil { + return nil, "", err + } - b, err = sbom.GenerateCycloneDX(b) - if err != nil { - return nil, "", err + b, err = sbom.GenerateImageCycloneDX(b) + if err != nil { + return nil, "", err + } + return b, ctypes.CycloneDXJSONMediaType, nil + + case oci.SignedImageIndex: + b, err := sbom.GenerateIndexCycloneDX(obj) + return b, ctypes.SPDXJSONMediaType, err + + default: + return nil, "", fmt.Errorf("unrecognized type: %T", se) } - return b, ctypes.CycloneDXJSONMediaType, nil } } @@ -810,7 +832,7 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl si := signed.Image(image) if g.sbom != nil { - sbom, mt, err := g.sbom(ctx, file, appPath, image) + sbom, mt, err := g.sbom(ctx, file, appPath, si) if err != nil { return nil, err } @@ -992,8 +1014,23 @@ func (g *gobuild) buildAll(ctx context.Context, ref string, baseIndex v1.ImageIn im.Annotations).(v1.ImageIndex), adds...) - // TODO(mattmoor): If we want to attach anything (e.g. signatures, attestations, SBOM) - // at the index level, we would do it here! + if g.sbom != nil { + sbom, mt, err := g.sbom(ctx, "", "", idx) + if err != nil { + return nil, err + } + if sbom != nil { + f, err := static.NewFile(sbom, static.WithLayerMediaType(mt)) + if err != nil { + return nil, err + } + idx, err = ocimutate.AttachFileToImageIndex(idx, "sbom", f) + if err != nil { + return nil, err + } + } + } + return idx, nil } diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index 0bd93af43c..afe8e22be6 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -392,14 +392,14 @@ func TestBuildConfig(t *testing.T) { } } -func nilGetBase(_ context.Context, _ string) (name.Reference, Result, error) { +func nilGetBase(context.Context, string) (name.Reference, Result, error) { return nil, nil, nil } 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, _ v1.Image) ([]byte, types.MediaType, error) { +func fauxSBOM(context.Context, string, string, oci.SignedEntity) ([]byte, types.MediaType, error) { return []byte(wantSBOM), "application/vnd.garbage", nil } diff --git a/pkg/commands/deps.go b/pkg/commands/deps.go index 1d82b8e4f0..5f6ecccc7e 100644 --- a/pkg/commands/deps.go +++ b/pkg/commands/deps.go @@ -30,6 +30,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/ko/internal/sbom" + "github.com/sigstore/cosign/pkg/oci/signed" "github.com/spf13/cobra" ) @@ -132,19 +133,15 @@ If the image was not built using ko, or if it was built without embedding depend []byte(n), []byte(path.Join("/ko-app", filepath.Base(filepath.Clean(h.Name)))), 1) - imgDigest, err := img.Digest() - if err != nil { - return err - } switch sbomType { case "spdx": - b, err := sbom.GenerateSPDX(Version, cfg.Created.Time, mod, imgDigest) + b, err := sbom.GenerateImageSPDX(Version, mod, signed.Image(img)) if err != nil { return err } io.Copy(os.Stdout, bytes.NewReader(b)) case "cyclonedx": - b, err := sbom.GenerateCycloneDX(mod) + b, err := sbom.GenerateImageCycloneDX(mod) if err != nil { return err }