diff --git a/.gitignore b/.gitignore index ee0fbb12..fb433ca1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ test/log .idea/ profile.cov .vscode/ +.aider* diff --git a/attestation/product/product.go b/attestation/product/product.go index 1754d841..fdd26d49 100644 --- a/attestation/product/product.go +++ b/attestation/product/product.go @@ -15,6 +15,7 @@ package product import ( + "bytes" "encoding/json" "fmt" "io" @@ -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) @@ -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 } diff --git a/attestation/sbom/sbom.go b/attestation/sbom/sbom.go new file mode 100644 index 00000000..33ae7ed2 --- /dev/null +++ b/attestation/sbom/sbom.go @@ -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") +} diff --git a/go.mod b/go.mod index bd856a11..b0c05457 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 84f5b584..7e85cceb 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/imports.go b/imports.go index c368d889..5ce58b22 100644 --- a/imports.go +++ b/imports.go @@ -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"