Skip to content

Commit

Permalink
improvements to rekor cataloger code (#1175)
Browse files Browse the repository at this point in the history
Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
  • Loading branch information
mdeicas authored and spiffcs committed Oct 25, 2022
1 parent 398b039 commit 08d26fc
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 97 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,4 +653,4 @@ The rekor-cataloger searches Rekor by hash for binaries and performs verificatio

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 ```.
To enable the rekor-cataloger, use the flag ``` --external-sources-enabled=true ```.
37 changes: 37 additions & 0 deletions cmd/syft/cli/eventloop/tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"fmt"

"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/rekor"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
Expand All @@ -24,6 +26,7 @@ func Tasks(app *config.Application) ([]Task, error) {
generateCatalogSecretsTask,
generateCatalogFileClassificationsTask,
generateCatalogContentsTask,
generateRekorCatalogerTask,
}

for _, generator := range generators {
Expand Down Expand Up @@ -60,6 +63,40 @@ func generateCatalogPackagesTask(app *config.Application) (Task, error) {
return task, nil
}

func generateRekorCatalogerTask(app *config.Application) (Task, error) {
if !app.ExternalSources.ExternalSourcesEnabled {
log.Debugf("skipping rekor cataloger task because external sources are disabled")
return nil, nil
}
if !app.RekorCataloger.Cataloger.Enabled {
log.Debugf("skipping rekor cataloger task because the rekor cataloger is disabled")
return nil, nil
}

client, err := rekor.NewClient()
if err != nil {
return nil, fmt.Errorf("could create client for Rekor cataloger")
}

scope := app.RekorCataloger.Cataloger.ScopeOpt
rekorCataloger := file.NewRekorCataloger(client)

task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(scope)
if err != nil {
return nil, err
}

rels, err := rekorCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
return rels, nil
}

return task, nil
}

func generateCatalogFileMetadataTask(app *config.Application) (Task, error) {
if !app.FileMetadata.Cataloger.Enabled {
return nil, nil
Expand Down
1 change: 1 addition & 0 deletions internal/config/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Application struct {
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"`
FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"`
RekorCataloger rekorCataloger `yaml:"rekor-cataloger" json:"rekor-cataloger" mapstructure:"rekor-cataloger"`
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Expand Down
21 changes: 21 additions & 0 deletions internal/config/rekor_cataloger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package config

import (
"github.com/anchore/syft/syft/source"
"github.com/spf13/viper"
)

var rekorCatalogerEnabledDefault bool = false

type rekorCataloger struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}

func (cfg rekorCataloger) loadDefaultValues(v *viper.Viper) {
v.SetDefault("rekor-cataloger.cataloger.enabled", rekorCatalogerEnabledDefault)
v.SetDefault("rekor-cataloger.cataloger.scope", source.SquashedScope)
}

func (cfg *rekorCataloger) parseConfigValues() error {
return cfg.Cataloger.parseConfigValues()
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package rekor
package file

import (
"fmt"

"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
rekorLib "github.com/anchore/syft/syft/rekor"
"github.com/anchore/syft/syft/rekor"
"github.com/anchore/syft/syft/source"
)

Expand All @@ -16,10 +15,12 @@ import (

const catalogerName = "rekor-cataloger"

type Cataloger struct{}
type Cataloger struct {
client *rekor.Client
}

func NewRekorCataloger() *Cataloger {
return &Cataloger{}
func NewRekorCataloger(client *rekor.Client) *Cataloger {
return &Cataloger{client: client}
}

func (c *Cataloger) Name() string {
Expand All @@ -30,26 +31,25 @@ func (c *Cataloger) UsesExternalSources() bool {
return true
}

func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]artifact.Relationship, error) {
var catalogedRels []artifact.Relationship
locations, err := resolver.FilesByMIMEType(internal.ExecutableMIMETypeSet.List()...)
if err != nil {
return nil, nil, fmt.Errorf("failed to find binaries by mime types: %w", err)
return nil, fmt.Errorf("failed to find binaries by mime types: %w", err)
}

client, err := rekorLib.NewClient()
if err != nil {
return nil, nil, fmt.Errorf("unable to get client: %w", err)
return nil, fmt.Errorf("unable to get client: %w", err)
}

for _, location := range locations {
rels, err := rekorLib.CreateRekorSbomRels(resolver, location, client)
rels, err := rekor.CreateRekorSbomRels(resolver, location, c.client)
if err != nil {
log.Debugf("Rekor cataloger failed to create relationships: %w", err)
continue
}
catalogedRels = append(catalogedRels, rels...)
}

return nil, catalogedRels, nil
return catalogedRels, nil
}
2 changes: 1 addition & 1 deletion syft/formats/spdx22json/to_format_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func isValidExternalRelationshipDocument(rel artifact.Relationship) (bool, error
}
if externalRef, ok := rel.To.(rekor.ExternalRef); ok {
relationshipType := artifact.DescribedByRelationship
if rel.Type == relationshipType && toChecksumAlgorithm(externalRef.SpdxRef.Alg) == "SHA1" {
if rel.Type == relationshipType && toChecksumAlgorithm(externalRef.SpdxRef.Alg) == "SHA1" { // spdx 2.2 spec requires an sha1 hash
return true, nil
}
return false, fmt.Errorf("syft cannot handle an ExternalRef with relationship type: %v", relationshipType)
Expand Down
54 changes: 18 additions & 36 deletions syft/rekor/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"crypto/sha1" //nolint:gosec // sha1 needed for checksums in spdx format
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -22,29 +21,19 @@ import (

// getSbom attempts to retrieve the SBOM from the URI.
//
// Precondition: client is not nil and validateAttestation has been called on att
// Precondition: client is not nil
//
// Postcondition: if no error, the returned byte list is not empty
func getSbom(att *InTotoAttestation, client *http.Client) (*[]byte, error) {
if len(att.Predicate.Sboms) > 1 {
log.Info("attestation found on Rekor with multiple SBOMS, which is not currently supported. Proceeding with the first SBOM.")
}
uri := att.Predicate.Sboms[0].URI
if uri == "" {
return nil, errors.New("uri of sbom is empty")
}

resp, err := client.Get(uri)
func getSbom(sbomEntry sbomEntry, client *http.Client) (*[]byte, error) {
resp, err := client.Get(sbomEntry.URI)
if err != nil {
return nil, fmt.Errorf("error making http request: %w", err)
}

bytes, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("error reading http response: %w", err)
}

if len(bytes) == 0 {
return nil, fmt.Errorf("retrieved sbom is empty")
}
Expand Down Expand Up @@ -79,33 +68,29 @@ func getAndVerifyRekorEntry(uuid string, client *client.Rekor) (*models.LogEntry
return nil, err
}

logEntry := res.Payload
if len(logEntry) == 0 {
var logEntryAnon models.LogEntryAnon
switch len(res.Payload) {
case 0:
return nil, fmt.Errorf("retrieved rekor entry has no logEntryAnons")
}
if len(logEntry) > 1 {
case 1:
for _, val := range res.Payload {
logEntryAnon = val
}
default:
return nil, fmt.Errorf("retrieved rekor entry has more than one logEntry")
}

// logEntry is a map from uuids to logEntryAnons
var logEntryAnon *models.LogEntryAnon
for _, val := range logEntry {
//nolint:gosec,exportloopref
logEntryAnon = &val // only one iteration of loop; no aliasing occurs
}

if logEntryAnon.LogIndex == nil {
return nil, fmt.Errorf("retrieved rekor entry has no log index")
}
logIndex := *logEntryAnon.LogIndex
log.Debugf("rekor entry %v was retrieved", logIndex)

ctx := context.Background()
if err = cosign.VerifyTLogEntry(ctx, client, logEntryAnon); err != nil {
if err = cosign.VerifyTLogEntry(ctx, client, &logEntryAnon); err != nil {
return nil, fmt.Errorf("could not prove that the log entry is on rekor: %w", err)
}

return logEntryAnon, nil
return &logEntryAnon, nil
}

// Precondition: client and its fields are not nil.
Expand All @@ -125,19 +110,19 @@ func getAndVerifySbomFromUUID(uuid string, client *Client) (*sbomWithMetadata, e

log.Debugf("verification of rekor entry %v complete", logIndex)

att, err := parseAndValidateAttestation(logEntryAnon)
subject, sbomEntry, err := parseAndValidateAttestation(logEntryAnon)
if err != nil {
return nil, fmt.Errorf("error parsing or validating attestation associated with rekor entry %v: %w", logIndex, err)
}

sbomBytes, err := getSbom(att, client.httpClient)
sbomBytes, err := getSbom(sbomEntry, client.httpClient)
if err != nil {
return nil, fmt.Errorf("error retrieving sbom from rekor entry %v: %w", logIndex, err)
}

log.Debugf("SBOM (%v bytes) retrieved", len(*sbomBytes))

if err = verifySbomHash(att, sbomBytes); err != nil {
if err = verifySbomHash(sbomEntry, sbomBytes); err != nil {
return nil, fmt.Errorf("could not verify retrieved sbom (from rekor entry %v): %w", logIndex, err)
}

Expand All @@ -150,12 +135,11 @@ func getAndVerifySbomFromUUID(uuid string, client *Client) (*sbomWithMetadata, e
}

sbomWrapped := &sbomWithMetadata{
executableSha256: att.Subject[0].Digest["sha256"],
executableSha256: subject.Digest["sha256"],
sha1: decodedHash,
rekorEntry: uuid,
spdx: sbom,
}

return sbomWrapped, nil
}

Expand All @@ -165,7 +149,7 @@ func getAndVerifySbomFromUUID(uuid string, client *Client) (*sbomWithMetadata, e
func getAndVerifySbomsFromHash(sha256 string, client *Client) ([]*sbomWithMetadata, error) {
uuids, err := getUuids(sha256, client.rekorClient)
if err != nil {
return nil, fmt.Errorf("error getting uuids on rekor associated with hash \"%v\": %w", sha256, err)
return nil, fmt.Errorf("error getting uuids on rekor associated with hash %q: %w", sha256, err)
}

var sboms []*sbomWithMetadata
Expand All @@ -181,7 +165,6 @@ func getAndVerifySbomsFromHash(sha256 string, client *Client) ([]*sbomWithMetada
}
sboms = append(sboms, sbom)
}

return sboms, nil
}

Expand All @@ -206,6 +189,5 @@ func getAndVerifySbomsFromResolver(resolver source.FileResolver, location source
if err != nil {
return nil, fmt.Errorf("error searching rekor in location %v: %w", location.RealPath, err)
}

return sboms, nil
}
10 changes: 2 additions & 8 deletions syft/rekor/rekor.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,6 @@ func NewExternalRef(docRef string, uri string, alg spdx.ChecksumAlgorithm, hash
}
}

func warnInfoForUser(uuids []string) {
s := fmt.Sprintf("%s\t%v\n", InfoForUser, uuids)
log.Warn(s)
}

// CreateRekorSbomRels searches Rekor by the hash of the file in the given location and creates external reference relationships
// for any sboms that are found and verified
func CreateRekorSbomRels(resolver source.FileResolver, location source.Location, client *Client) ([]artifact.Relationship, error) {
Expand Down Expand Up @@ -113,11 +108,10 @@ func CreateRekorSbomRels(resolver source.FileResolver, location source.Location,
}
rels = append(rels, *rel)
usedRekorEntries = append(usedRekorEntries, sbomWithDigest.rekorEntry)
log.Debug("relationship created for SBOM found on rekor")
log.Debugf("relationship created for SBOM found on rekor: %+v", *rel)
}
if len(rels) > 0 {
warnInfoForUser(usedRekorEntries)
log.Warn(fmt.Sprintf("%s\t%v\n", InfoForUser, usedRekorEntries))
}

return rels, nil
}
Loading

0 comments on commit 08d26fc

Please sign in to comment.