Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SBOM Discovery with Rekor #1157

Merged
merged 8 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ registry:yourrepo/yourimage:tag pull image directly from a registry (no

#### Non Default:
- cargo-auditable-binary
- rekor
mdeicas marked this conversation as resolved.
Show resolved Hide resolved

### Excluding file paths

Expand Down Expand Up @@ -663,3 +664,12 @@ The following checks were performed on each of these signatures:
```

Consumers of your image can now trust that the SBOM associated with your image is correct and from a trusted source.

## Discovery of SBOMs on Rekor (experimental)
Syft can search the Rekor transparency log for SBOM attestations of binaries it finds while scanning and incorporate the results into the SBOMs it produces. This allows the use of SBOMs produced at build time (such as by a trusted builder), to augment binary metadata in the resultant SBOM.

The rekor-cataloger searches Rekor by hash for binaries and performs verification to ensure that the SBOMs and attestations have not been tampered with. It verifies the log entry has been signed by Rekor's public key, the certificate associated with the log entry chains back to the Fulcio root certificate, the log entry timestamp lies in the period of validity of the certificate, and other verifications. In the SBOM that Syft produces, the information is represented as an external document reference containing the URI and hash of the SBOM.

This is an experimental feature. It uses external sources, a functionality that is new to Syft. The use of trusted builders to produce SBOMs has not yet been fully established, and more consideration of what external sources to trust is necessary. Currently, Syft accepts any SBOM attestation that has a valid certificate issued by Fulcio.

To enable the rekor-cataloger, use the flag ``` --catalogers rekor ```.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ require (
require (
github.com/Masterminds/sprig/v3 v3.2.2
github.com/docker/docker v20.10.17+incompatible
github.com/go-openapi/runtime v0.24.1
github.com/google/go-containerregistry v0.11.0
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add
github.com/knqyf263/go-rpmdb v0.0.0-20220629110411-9a3bd2ebb923
Expand Down Expand Up @@ -140,7 +141,6 @@ require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/loads v0.21.1 // indirect
github.com/go-openapi/runtime v0.24.1 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
Expand Down
12 changes: 12 additions & 0 deletions internal/formats/spdx22json/model/doc_element_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package model

import "github.com/anchore/syft/internal/formats/common/spdxhelpers"

// DocElementID represents the identifier string portion of an SPDX document
// identifier. It should be to used to reference any other SPDX document.
// DocElementIDs should NOT contain the 'DOCUMENTREF-' portion.
type DocElementID string

func (d DocElementID) String() string {
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
return "DocumentRef-" + spdxhelpers.SanitizeElementID(string(d))
}
2 changes: 1 addition & 1 deletion internal/formats/spdx22json/model/external_document_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ type ExternalDocumentRef struct {
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
ExternalDocumentID string `json:"externalDocumentId"`
Checksum Checksum `json:"checksum"`
// SPDX ID for SpdxDocument. A propoerty containing an SPDX document.
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
// SPDX ID for SpdxDocument. A property containing an SPDX document.
SpdxDocument string `json:"spdxDocument"`
}
86 changes: 73 additions & 13 deletions internal/formats/spdx22json/to_format_model.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package spdx22json

import (
"errors"
"fmt"
"sort"
"strings"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/rekor"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
Expand All @@ -37,14 +39,55 @@ func toFormatModel(s sbom.SBOM) *model.Document {
},
LicenseListVersion: spdxlicense.Version,
},
DataLicense: "CC0-1.0",
DocumentNamespace: namespace,
Packages: toPackages(s.Artifacts.PackageCatalog, s.Relationships),
Files: toFiles(s),
Relationships: toRelationships(s.Relationships),
DataLicense: "CC0-1.0",
ExternalDocumentRefs: toExternalDocumentRefs(s.Relationships),
DocumentNamespace: namespace,
Packages: toPackages(s.Artifacts.PackageCatalog, s.Relationships),
Files: toFiles(s),
Relationships: toRelationships(s.Relationships),
}
}

// isValidExternalRelationshipDocument returns if rel contains an ExternalRef and if it to_format_model know how to handle it.
// An error is returned if rel contains an ExternalRef, but the rel cannot be handled
func isValidExternalRelationshipDocument(rel artifact.Relationship) (bool, error) {
if _, ok := rel.From.(rekor.ExternalRef); ok {
return false, errors.New("syft cannot handle an ExternalRef in the FROM field of a relationship")
}
if externalRef, ok := rel.To.(rekor.ExternalRef); ok {
relationshipType := artifact.DescribedByRelationship
if rel.Type == relationshipType && toChecksumAlgorithm(externalRef.SpdxRef.Alg) == "SHA1" {
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
return true, nil
}
return false, fmt.Errorf("syft cannot handle an ExternalRef with relationship type: %v", relationshipType)
}
return false, nil
}

func toExternalDocumentRefs(relationships []artifact.Relationship) []model.ExternalDocumentRef {
externalDocRefs := []model.ExternalDocumentRef{}
for _, rel := range relationships {
valid, err := isValidExternalRelationshipDocument(rel)
if err != nil {
log.Warnf("dropping relationship %v: %w", rel, err)
continue
}
if valid {
externalRef := rel.To.(rekor.ExternalRef)
externalDocRef := model.ExternalDocumentRef{
ExternalDocumentID: model.DocElementID(rel.To.ID()).String(),
Checksum: model.Checksum{
Algorithm: toChecksumAlgorithm(externalRef.SpdxRef.Alg),
ChecksumValue: externalRef.SpdxRef.Checksum,
},
SpdxDocument: externalRef.SpdxRef.URI,
}
externalDocRefs = append(externalDocRefs, externalDocRef)
}
}
return externalDocRefs
}

func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []model.Package {
packages := make([]model.Package, 0)

Expand Down Expand Up @@ -216,21 +259,34 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) {
return ty
}

func toRelationships(relationships []artifact.Relationship) (result []model.Relationship) {
func toRelationships(relationships []artifact.Relationship) []model.Relationship {
result := []model.Relationship{}
for _, r := range relationships {
exists, relationshipType, comment := lookupRelationship(r.Type)

if !exists {
log.Warnf("unable to convert relationship from SPDX 2.2 JSON, dropping: %+v", r)
continue
}

result = append(result, model.Relationship{
SpdxElementID: model.ElementID(r.From.ID()).String(),
RelationshipType: relationshipType,
RelatedSpdxElement: model.ElementID(r.To.ID()).String(),
Comment: comment,
})
rel := model.Relationship{
SpdxElementID: model.ElementID(r.From.ID()).String(),
RelationshipType: relationshipType,
Comment: comment,
}

// if this relationship contains an external document ref, we need to use DocElementID instead of ElementID
valid, err := isValidExternalRelationshipDocument(r)
if err != nil {
log.Warnf("dropping relationship %v: %w", rel, err)
continue
}
if valid {
rel.RelatedSpdxElement = model.DocElementID(r.To.ID()).String()
} else {
rel.RelatedSpdxElement = model.ElementID(r.To.ID()).String()
}

result = append(result, rel)
}
return result
}
Expand All @@ -241,6 +297,10 @@ func lookupRelationship(ty artifact.RelationshipType) (bool, spdxhelpers.Relatio
return true, spdxhelpers.ContainsRelationship, ""
case artifact.OwnershipByFileOverlapRelationship:
return true, spdxhelpers.OtherRelationship, fmt.Sprintf("%s: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", ty)
case artifact.DependencyOfRelationship:
return true, spdxhelpers.DependencyOfRelationship, ""
case artifact.DescribedByRelationship:
return true, spdxhelpers.DescribedByRelationship, ""
}
return false, "", ""
}
Loading