Skip to content

Commit

Permalink
Feat/SBOM attestor (#268)
Browse files Browse the repository at this point in the history
* feat(sbom-attestor): add SBOM attestor for SPDX and CycloneDX formats

---------
Signed-off-by: John Kjell <[email protected]>
Co-authored-by: Cole <[email protected]>
Co-authored-by: Tom Meadows <[email protected]>
  • Loading branch information
jkjell authored Jun 11, 2024
1 parent ced56ab commit 26d4ac1
Show file tree
Hide file tree
Showing 14 changed files with 8,719 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/verify-licence.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ jobs:
- name: Check license headers
run: |
set -e
addlicense --check -l apache -c 'The Witness Contributors' -v *
addlicense --check -l apache -c 'The Witness Contributors' --ignore "**/*.xml" -v *
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*
78 changes: 25 additions & 53 deletions attestation/product/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@
package product

import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"

"github.com/gabriel-vasile/mimetype"
"github.com/gobwas/glob"
"github.com/in-toto/go-witness/attestation"
"github.com/in-toto/go-witness/attestation/file"
Expand Down Expand Up @@ -118,17 +115,13 @@ type Attestor struct {
compiledExcludeGlob glob.Glob
}

func fromDigestMap(digestMap map[string]cryptoutil.DigestSet) map[string]attestation.Product {
func fromDigestMap(workingDir string, digestMap map[string]cryptoutil.DigestSet) map[string]attestation.Product {
products := make(map[string]attestation.Product)
for fileName, digestSet := range digestMap {
mimeType := "unknown"
f, err := os.OpenFile(fileName, os.O_RDONLY, 0666)
if err == nil {
mimeType, err = getFileContentType(f)
if err != nil {
mimeType = "unknown"
}
f.Close()
filePath := workingDir + fileName
mimeType, err := getFileContentType(filePath)
if err != nil {
mimeType = "unknown"
}

products[fileName] = attestation.Product{
Expand Down Expand Up @@ -192,7 +185,7 @@ func (a *Attestor) Attest(ctx *attestation.AttestationContext) error {
return err
}

a.products = fromDigestMap(products)
a.products = fromDigestMap(ctx.WorkingDir(), products)
return nil
}

Expand Down Expand Up @@ -231,47 +224,26 @@ func (a *Attestor) Subjects() map[string]cryptoutil.DigestSet {
return subjects
}

func getFileContentType(file *os.File) (string, error) {
// Read up to 512 bytes from the file.
buffer := make([]byte, 512)
_, err := file.Read(buffer)
if err != nil && err != io.EOF {
return "", err
}
func getFileContentType(fileName string) (string, error) {
// Add SPDX JSON detector
mimetype.Lookup("application/json").Extend(func(buf []byte, limit uint32) bool {
return bytes.HasPrefix(buf, []byte(`{"spdxVersion": "SPDX-`))
}, "application/spdx+json", ".spdx.json")

// Try to detect the content type using http.DetectContentType().
contentType := http.DetectContentType(buffer)

// 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)
}
}
// Add CycloneDx JSON detector
mimetype.Lookup("application/json").Extend(func(buf []byte, limit uint32) bool {
return bytes.HasPrefix(buf, []byte(`{"$schema": "http://cyclonedx.org/schema/bom-`))
}, "application/vnd.cyclonedx+json", ".cdx.json")

return contentType, nil
}
// Add CycloneDx XML detector
mimetype.Lookup("text/xml").Extend(func(buf []byte, limit uint32) bool {
return bytes.HasPrefix(buf, []byte(`<?xml version="1.0" encoding="UTF-8"?><bom xmlns="http://cyclonedx.org/schema/bom/`))
}, "application/vnd.cyclonedx+xml", ".cdx.xml")

// getFileSignature tries to match the file signature to a content type.
func getFileSignature(buffer []byte) (string, error) {
// 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)

var signature string
switch {
// https://en.wikipedia.org/wiki/List_of_file_signatures
case buffer[257] == 0x75 && buffer[258] == 0x73 && buffer[259] == 0x74 && buffer[260] == 0x61 && buffer[261] == 0x72:
signature = "application/x-tar"
case buffer[0] == 0x25 && buffer[1] == 0x50 && buffer[2] == 0x44 && buffer[3] == 0x46 && buffer[4] == 0x2D:
signature = "application/pdf"
default:
// If the file signature is not recognized, return application/octet-stream by default
signature = "application/octet-stream"
contentType, err := mimetype.DetectFile(fileName)
if err != nil {
return "", err
}

return signature, nil
return contentType.String(), nil
}
4 changes: 2 additions & 2 deletions attestation/product/product_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestFromDigestMap(t *testing.T) {
assert.NoError(t, err)
testDigestSet := make(map[string]cryptoutil.DigestSet)
testDigestSet["test"] = testDigest
result := fromDigestMap(testDigestSet)
result := fromDigestMap("", testDigestSet)
assert.Len(t, result, 1)
digest := result["test"].Digest
assert.True(t, digest.Equal(testDigest))
Expand Down Expand Up @@ -126,7 +126,7 @@ func TestGetFileContentType(t *testing.T) {
// Run the test cases.
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
contentType, err := getFileContentType(test.file)
contentType, err := getFileContentType(test.file.Name())
require.NoError(t, err)
require.Equal(t, test.expected, contentType)
})
Expand Down
Loading

0 comments on commit 26d4ac1

Please sign in to comment.