Skip to content

Commit

Permalink
Add SBOM Discovery with Rekor (#1157)
Browse files Browse the repository at this point in the history
This PR adds the ability to discover build-time SBOMs from binaries with the Rekor transparency log.
It does this by creating external document references for them in SPDX JSON.

Explained in more detail in syft issue #1159

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
  • Loading branch information
mdeicas authored and spiffcs committed Oct 21, 2022
1 parent 6949a25 commit 3f91eae
Show file tree
Hide file tree
Showing 40 changed files with 41,015 additions and 15 deletions.
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

### 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 {
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.
// 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" {
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

0 comments on commit 3f91eae

Please sign in to comment.