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

feat(sbom-attestor): add SBOM attestor for SPDX and CycloneDX formats #231

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ test/log
.idea/
profile.cov
.vscode/
.aider*
21 changes: 13 additions & 8 deletions attestation/product/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package product

import (
"bytes"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -221,19 +222,19 @@ func getFileContentType(file *os.File) (string, error) {

// If the content type is application/octet-stream, try to detect the content type using a file signature.
if contentType == "application/octet-stream" {
// Try to match the file signature to a content type.
if signature, _ := getFileSignature(buffer); signature != "application/octet-stream" {
contentType = signature
} else if extension := filepath.Ext(file.Name()); extension != "" {
contentType = mime.TypeByExtension(extension)
contentType = detectFileSignature(buffer)
if contentType == "application/octet-stream" {
if extension := filepath.Ext(file.Name()); extension != "" {
contentType = mime.TypeByExtension(extension)
}
}
}

return contentType, nil
}

// getFileSignature tries to match the file signature to a content type.
func getFileSignature(buffer []byte) (string, error) {
// detectFileSignature tries to match the file signature to a content type.
func detectFileSignature(buffer []byte) string {
// Create a new buffer with a length of 512 bytes and copy the data from the input buffer into the new buffer to prevent out of bounds errors.
newBuffer := make([]byte, 512)
copy(newBuffer, buffer)
Expand All @@ -245,10 +246,14 @@ func getFileSignature(buffer []byte) (string, error) {
signature = "application/x-tar"
case buffer[0] == 0x25 && buffer[1] == 0x50 && buffer[2] == 0x44 && buffer[3] == 0x46 && buffer[4] == 0x2D:
signature = "application/pdf"
case bytes.HasPrefix(buffer, []byte(`{"spdxVersion":"SPDX-`)):
signature = "application/spdx+json"
case bytes.HasPrefix(buffer, []byte(`{"$schema":"http://cyclonedx.org/schema/bom-`)):
signature = "application/vnd.cyclonedx+json"
default:
// If the file signature is not recognized, return application/octet-stream by default
signature = "application/octet-stream"
}

return signature, nil
return signature
}
104 changes: 104 additions & 0 deletions attestation/sbom/sbom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package sbom

import (
"encoding/json"
"fmt"
"io"
"os"

"github.com/in-toto/go-witness/attestation"
"github.com/in-toto/go-witness/cryptoutil"
"github.com/in-toto/go-witness/log"
)

const (
Name = "sbom"
Type = "https://witness.dev/attestations/sbom/v0.1"
RunType = attestation.PostProductRunType

spdxMimeType = "application/spdx+json"
cycloneDxMimeType = "application/vnd.cyclonedx+json"
)

func init() {
attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor {
return NewSBOMAttestor()
})
}

type SBOMAttestor struct {
SBOMDocument interface{} `json:"sbomDocument"`
SBOMFile string `json:"sbomFileName"`
SBOMDigestSet cryptoutil.DigestSet `json:"sbomDigestSet"`
}

func NewSBOMAttestor() *SBOMAttestor {
return &SBOMAttestor{}
}

func (a *SBOMAttestor) Name() string {
return Name
}

func (a *SBOMAttestor) Type() string {
return Type
}

func (a *SBOMAttestor) RunType() attestation.RunType {
return RunType
}

func (a *SBOMAttestor) Attest(ctx *attestation.AttestationContext) error {
if err := a.getCandidate(ctx); err != nil {
log.Debugf("(attestation/sbom) error getting candidate: %w", err)
return err
}

return nil
}

func (a *SBOMAttestor) getCandidate(ctx *attestation.AttestationContext) error {
products := ctx.Products()

if len(products) == 0 {
return fmt.Errorf("no products to attest")
}

for path, product := range products {
if product.MimeType != spdxMimeType && product.MimeType != cycloneDxMimeType {
continue
}

newDigestSet, err := cryptoutil.CalculateDigestSetFromFile(path, ctx.Hashes())
if newDigestSet == nil || err != nil {
return fmt.Errorf("error calculating digest set from file: %s", path)
}

if !newDigestSet.Equal(product.Digest) {
return fmt.Errorf("integrity error: product digest set does not match candidate digest set")
}

f, err := os.Open(path)
if err != nil {
return fmt.Errorf("error opening file: %s", path)
}

sbomBytes, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("error reading file: %s", path)
}

var sbomDocument interface{}
if err := json.Unmarshal(sbomBytes, &sbomDocument); err != nil {
log.Debugf("(attestation/sbom) error unmarshaling SBOM: %w", err)
continue
}

a.SBOMFile = path
a.SBOMDigestSet = product.Digest
a.SBOMDocument = sbomDocument

return nil
}
return fmt.Errorf("no SBOM file found")
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ require (
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/zclconf/go-cty v1.14.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ github.com/spiffe/go-spiffe/v2 v2.1.7/go.mod h1:QJDGdhXllxjxvd5B+2XnhhXB/+rC8gr+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
_ "github.com/in-toto/go-witness/attestation/oci"
_ "github.com/in-toto/go-witness/attestation/product"
_ "github.com/in-toto/go-witness/attestation/sarif"
_ "github.com/in-toto/go-witness/attestation/sbom"

// signer providers
_ "github.com/in-toto/go-witness/signer/file"
Expand Down
Loading