pkg/sbom.go (184 lines of code) (raw):
package obom
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
purl "github.com/package-url/packageurl-go"
spdxjson "github.com/spdx/tools-golang/json"
v2common "github.com/spdx/tools-golang/spdx/v2/common"
"github.com/spdx/tools-golang/spdx/v2/v2_3"
)
const (
MEDIATYPE_SPDX = "application/spdx+json"
OCI_ANNOTATION_DOCUMENT_NAME = "org.spdx.name"
OCI_ANNOTATION_DOCUMENT_NAMESPACE = "org.spdx.namespace"
OCI_ANNOTATION_SPDX_VERSION = "org.spdx.version"
OCI_ANNOTATION_CREATION_DATE = "org.spdx.created"
OCI_ANNOTATION_CREATORS = "org.spdx.creator"
)
type SPDXDocument struct {
// Version is the version of the SPDX specification used in the document
Version string `json:"spdxVersion"`
Document *v2_3.Document `json:"document"`
}
// LoadSBOMFromFile opens a file given by filename, reads its contents, and loads it into an SPDX document.
// It also calculates the file size and generates an OCI descriptor for the file.
// It returns the loaded SPDX document, the OCI descriptor, and any error encountered.
func LoadSBOMFromFile(filename string, strict bool) (*SPDXDocument, *ocispec.Descriptor, []byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, nil, nil, err
}
defer file.Close()
return LoadSBOMFromReader(file, strict)
}
// LoadSBOMFromReader reads an SPDX document from an io.ReadCloser, generates an OCI descriptor for the document,
// and returns the loaded SPDX document and the OCI descriptor.
// If an error occurs during reading the document or generating the descriptor, the error will be returned.
func LoadSBOMFromReader(reader io.ReadCloser, strict bool) (*SPDXDocument, *ocispec.Descriptor, []byte, error) {
defer reader.Close()
desc, sbomBytes, err := LoadArtifactFromReader(reader, MEDIATYPE_SPDX)
if err != nil {
return nil, nil, nil, err
}
doc, err := getSPDXDocumentFromSBOMBytes(sbomBytes, strict)
if err != nil {
return nil, nil, nil, err
}
return doc, desc, sbomBytes, nil
}
func getSPDXDocumentFromSBOMBytes(sbomBytes []byte, strict bool) (*SPDXDocument, error) {
var jsonDoc map[string]interface{}
err := json.Unmarshal(sbomBytes, &jsonDoc)
if err != nil {
return nil, fmt.Errorf("error unmarshaling SBOM bytes: %w", err)
}
version, ok := jsonDoc["spdxVersion"].(string)
if !ok {
return nil, fmt.Errorf("SBOM does not contain spdxVersion field")
}
sbomReader := bytes.NewReader(sbomBytes)
doc, err := spdxjson.Read(sbomReader)
if err != nil && !strict {
fmt.Printf("Warning: error parsing SPDX document: %v. Falling back to simple JSON parsing.\n", err)
doc, err = GetSBOMFromMap(jsonDoc)
if err != nil {
return nil, fmt.Errorf("error parsing SPDX document from map: %w", err)
}
}
if err != nil && strict {
return nil, fmt.Errorf("error parsing SPDX document: %w", err)
}
return &SPDXDocument{Version: version, Document: doc}, nil
}
func GetSBOMFromMap(sbomMap map[string]interface{}) (*v2_3.Document, error) {
version, ok := sbomMap["spdxVersion"].(string)
if !ok {
return nil, fmt.Errorf("SBOM does not contain spdxVersion field")
}
namespace, ok := sbomMap["documentNamespace"].(string)
if !ok {
return nil, fmt.Errorf("SBOM does not contain documentNamespace field")
}
name, ok := sbomMap["name"].(string)
if !ok {
return nil, fmt.Errorf("SBOM does not contain name field")
}
return &v2_3.Document{
SPDXVersion: version,
DocumentNamespace: namespace,
DocumentName: name,
}, nil
}
// GetAnnotations returns the annotations from the SBOM
func GetAnnotations(sbomDoc *SPDXDocument) (map[string]string, error) {
sbom := sbomDoc.Document
version := sbomDoc.Version
annotations := make(map[string]string)
annotations[OCI_ANNOTATION_DOCUMENT_NAME] = sbom.DocumentName
annotations[OCI_ANNOTATION_DOCUMENT_NAMESPACE] = sbom.DocumentNamespace
annotations[OCI_ANNOTATION_SPDX_VERSION] = version
if sbom.CreationInfo != nil {
var creatorstrings []string
for _, creator := range sbom.CreationInfo.Creators {
creatorstrings = append(creatorstrings, fmt.Sprintf("%s: %s", creator.CreatorType, creator.Creator))
}
annotations[OCI_ANNOTATION_CREATION_DATE] = sbom.CreationInfo.Created
annotations[OCI_ANNOTATION_CREATORS] = strings.Join(creatorstrings, ", ")
}
return annotations, nil
}
// GetPackages returns the packages from the SBOM
func GetPackages(sbom *v2_3.Document) ([]string, error) {
var packages []string
for _, pkg := range sbom.Packages {
if pkg.PackageExternalReferences != nil {
for _, exRef := range pkg.PackageExternalReferences {
packages = append(packages, exRef.Locator)
}
}
}
return packages, nil
}
func GetFiles(sbom *v2_3.Document) ([]string, error) {
var files []string
for _, file := range sbom.Files {
files = append(files, file.FileName)
}
return files, nil
}
type SBOMSummary struct {
SbomSummary struct {
Files []string `json:"files"`
Packages []PackageSummary `json:"packages"`
} `json:"sbomSummary"`
}
type PackageSummary struct {
Name string `json:"name"`
Version string `json:"version"`
License string `json:"license"`
PackageManager string `json:"packageManager"`
}
func GetPackageSummary(pkg *v2_3.Package) (*PackageSummary, error) {
var packageSummary PackageSummary
packageSummary.Name = pkg.PackageName
packageSummary.Version = pkg.PackageVersion
packageSummary.License = pkg.PackageLicenseDeclared
packageManager, _ := GetPackageManager(pkg.PackageExternalReferences)
if packageManager != "" {
packageSummary.PackageManager = packageManager
}
return &packageSummary, nil
}
func GetPackageManager(externalReferences []*v2_3.PackageExternalReference) (string, error) {
for _, exRef := range externalReferences {
if exRef.Category == v2common.CategoryPackageManager && exRef.RefType == v2common.TypePackageManagerPURL {
packageUrl, err := purl.FromString(exRef.Locator)
if err != nil {
return "", fmt.Errorf("error parsing package url for %s: %v", exRef.Locator, err)
}
return packageUrl.Type, nil
}
}
return "", fmt.Errorf("no package manager found")
}
func GetPackageSummaries(sbom *v2_3.Document) ([]PackageSummary, error) {
var packageSummaries []PackageSummary
for _, pkg := range sbom.Packages {
packageSummary, err := GetPackageSummary(pkg)
if err != nil {
return nil, err
}
packageSummaries = append(packageSummaries, *packageSummary)
}
return packageSummaries, nil
}
func GetSBOMSummary(sbom *v2_3.Document) (*SBOMSummary, error) {
var sbomSummary SBOMSummary
files, err := GetFiles(sbom)
if err != nil {
return nil, err
}
packages, err := GetPackageSummaries(sbom)
if err != nil {
return nil, err
}
sbomSummary.SbomSummary.Files = files
sbomSummary.SbomSummary.Packages = packages
return &sbomSummary, nil
}