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 4 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 SBOMs 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), which can provide more information about a binary than a post-compilation analysis.
mdeicas marked this conversation as resolved.
Show resolved Hide resolved

The rekor-cataloger searches Rekor by hash for binaries and performs verification to ensure that the SBOMs and attestations have not been tampered with. In the SBOM that Syft produces, the information is represented as an external document reference containing the URI and hash of the SBOM.
mdeicas marked this conversation as resolved.
Show resolved Hide resolved

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 all ```.
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
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"`
}
62 changes: 50 additions & 12 deletions internal/formats/spdx22json/to_format_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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 +38,34 @@ 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),
}
}

func toExternalDocumentRefs(relationships []artifact.Relationship) []model.ExternalDocumentRef {
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
var externalRefs []model.ExternalDocumentRef

for _, rel := range relationships {
if externalRef, ok := rel.To.(rekor.ExternalRef); ok {
externalRefDocument := 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,
}
externalRefs = append(externalRefs, externalRefDocument)
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
}
}
return externalRefs
}

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

Expand Down Expand Up @@ -219,19 +240,32 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) {
func toRelationships(relationships []artifact.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
if _, ok := r.To.(rekor.ExternalRef); ok {
if r.Type == artifact.DescribedByRelationship {
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
rel.RelatedSpdxElement = model.DocElementID(r.To.ID()).String()
} else {
log.Warnf("Syft does not know how to describe the external reference relationship in SPDX JSON, dropping %+v", r)
continue
}
} else {
rel.RelatedSpdxElement = model.ElementID(r.To.ID()).String()
}

result = append(result, rel)
}

return result
}

Expand All @@ -241,6 +275,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, "", ""
}
170 changes: 170 additions & 0 deletions internal/formats/spdx22json/to_format_model_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package spdx22json

import (
"fmt"
"testing"

"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/rekor"
"github.com/anchore/syft/syft/sbom"

"github.com/anchore/syft/syft/file"

Expand All @@ -15,6 +18,173 @@ import (
"github.com/stretchr/testify/assert"
)

func Test_toRelationships(t *testing.T) {

package_1 := pkg.Package{Name: "Hello World Package 1"}
package_1.SetID()
package_2 := pkg.Package{Name: "Hello World Package 2"}
package_2.SetID()
externalRef_1 := rekor.NewExternalRef("HelloWorld", "www.example.com", "SHA1", "bogushash")
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
coordinates := source.Coordinates{
RealPath: "foobar path",
}

tests := []struct {
name string
relationships []artifact.Relationship
result []model.Relationship
}{
{
name: "both normal and external reference relationships",
relationships: []artifact.Relationship{
{
From: package_1,
Type: artifact.DependencyOfRelationship,
To: package_2,
},
{
From: coordinates,
Type: artifact.DescribedByRelationship,
To: externalRef_1,
},
},
result: []model.Relationship{
{
SpdxElementID: fmt.Sprint("SPDXRef-", package_1.ID()),
RelationshipType: spdxhelpers.DependencyOfRelationship,
RelatedSpdxElement: fmt.Sprint("SPDXRef-", package_2.ID()),
},
{
SpdxElementID: fmt.Sprint("SPDXRef-", coordinates.ID()),
RelationshipType: spdxhelpers.DescribedByRelationship,
RelatedSpdxElement: fmt.Sprint("DocumentRef-", externalRef_1.ID()),
},
},
},
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.result, toRelationships(test.relationships))
})
}
}

func Test_toExternalDocumentRefs(t *testing.T) {

package_1 := pkg.Package{Name: "Hello World Package 1"}
package_2 := pkg.Package{Name: "Hello World Package 2"}
externalRef_1 := rekor.NewExternalRef("HelloWorld", "www.example.com", "SHA1", "bogushash")
externalRef_2 := rekor.NewExternalRef("Test", "www.test.com", "sha1", "testhash")

tests := []struct {
name string
relationships []artifact.Relationship
expected []model.ExternalDocumentRef
}{
{
name: "empty",
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
},
{
name: "Both external relationships and non external relationships",
relationships: []artifact.Relationship{
{
From: package_1,
To: package_2,
Type: artifact.ContainsRelationship,
},
{
From: package_1,
To: externalRef_1,
Type: artifact.ContainsRelationship,
},
},
expected: []model.ExternalDocumentRef{
{
ExternalDocumentID: model.DocElementID(externalRef_1.ID()).String(),
Checksum: model.Checksum{Algorithm: "SHA1", ChecksumValue: "bogushash"},
SpdxDocument: externalRef_1.SpdxRef.URI,
},
},
},
{
name: "Lowercase checksum algorithm",
relationships: []artifact.Relationship{
{
From: package_1,
To: externalRef_2,
Type: artifact.ContainsRelationship,
},
},
expected: []model.ExternalDocumentRef{
{
ExternalDocumentID: model.DocElementID(externalRef_2.ID()).String(),
Checksum: model.Checksum{Algorithm: "SHA1", ChecksumValue: "testhash"},
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
SpdxDocument: externalRef_2.SpdxRef.URI,
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, toExternalDocumentRefs(test.relationships))
})
}
}

func Test_toFiles(t *testing.T) {
coordinates1 := source.Coordinates{RealPath: "hi there"}
coordinates2 := source.Coordinates{RealPath: "goodbye"}

tests := []struct {
name string
inputSbom sbom.SBOM
expectedFiles []model.File
}{
{
name: "files are created just from relationships",
inputSbom: sbom.SBOM{
Relationships: []artifact.Relationship{
{
From: coordinates1,
To: coordinates2,
},
},
},
expectedFiles: []model.File{
{
Item: model.Item{
Element: model.Element{SPDXID: model.ElementID(coordinates1.ID()).String()},
LicenseConcluded: "NOASSERTION",
},
FileName: "hi there",
},
{
Item: model.Item{
Element: model.Element{SPDXID: model.ElementID(coordinates2.ID()).String()},
LicenseConcluded: "NOASSERTION",
},
FileName: "goodbye",
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res := toFiles(test.inputSbom)
if len(res) != len(test.expectedFiles) {
assert.FailNowf(t, "", "unexpected number of files returned, expected %v, found %v", len(test.expectedFiles), len(res))
} else {
for _, file := range test.expectedFiles {
assert.Contains(t, res, file)
}
}
})
}
}

func Test_toFileTypes(t *testing.T) {

tests := []struct {
Expand Down
3 changes: 3 additions & 0 deletions syft/artifact/relationship.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const (

// DependencyOfRelationship is a proxy for the SPDX 2.2.1 DEPENDENCY_OF relationship.
DependencyOfRelationship RelationshipType = "dependency-of"

// DescribedByRelationship is a proxy for the SPDX 2.2.1 DESCRIBED_BY relationship.
DescribedByRelationship RelationshipType = "described-by"
)

type RelationshipType string
Expand Down
2 changes: 2 additions & 0 deletions syft/pkg/cataloger/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/php"
"github.com/anchore/syft/syft/pkg/cataloger/portage"
"github.com/anchore/syft/syft/pkg/cataloger/python"
"github.com/anchore/syft/syft/pkg/cataloger/rekor"
"github.com/anchore/syft/syft/pkg/cataloger/rpmdb"
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
"github.com/anchore/syft/syft/pkg/cataloger/rust"
Expand Down Expand Up @@ -114,6 +115,7 @@ func AllCatalogers(cfg Config) []Cataloger {
cpp.NewConanfileCataloger(),
portage.NewPortageCataloger(),
haskell.NewHackageCataloger(),
rekor.NewRekorCataloger(),
mdeicas marked this conversation as resolved.
Show resolved Hide resolved
}, cfg.Catalogers)
}

Expand Down
Loading