github.com/google/osv-scalibr@v0.4.1/converter/spdx/spdx.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package spdx provides utilities for creating SPDX SBOMs.
    16  package spdx
    17  
    18  import (
    19  	"fmt"
    20  	"regexp"
    21  	"time"
    22  
    23  	"bitbucket.org/creachadair/stringset"
    24  	"github.com/google/osv-scalibr/log"
    25  	"github.com/google/osv-scalibr/result"
    26  	"github.com/google/uuid"
    27  	"github.com/spdx/tools-golang/spdx/v2/common"
    28  	"github.com/spdx/tools-golang/spdx/v2/v2_3"
    29  )
    30  
    31  const (
    32  	// NoAssertion indicates that we don't claim anything about the value of a given field.
    33  	NoAssertion = "NOASSERTION"
    34  	// SPDXRefPrefix is the prefix used in reference IDs in the SPDX document.
    35  	SPDXRefPrefix = "SPDXRef-"
    36  	// SPDXDocumentID is the string identifier used to refer to the SPDX document.
    37  	SPDXDocumentID = "SPDXRef-DOCUMENT"
    38  )
    39  
    40  // spdx_id must only contain letters, numbers, "." and "-"
    41  var spdxIDInvalidCharRe = regexp.MustCompile(`[^a-zA-Z0-9.-]`)
    42  
    43  // Config describes custom settings that should be applied to the generated SPDX file.
    44  type Config struct {
    45  	DocumentName      string
    46  	DocumentNamespace string
    47  	Creators          []common.Creator
    48  }
    49  
    50  // ToSPDX23 converts the SCALIBR scan results into an SPDX v2.3 document.
    51  func ToSPDX23(r *result.ScanResult, c Config) *v2_3.Document {
    52  	packages := make([]*v2_3.Package, 0, len(r.Inventory.Packages)+1)
    53  
    54  	// Add a main package that contains all other top-level packages.
    55  	mainPackageID := SPDXRefPrefix + "Package-main-" + uuid.New().String()
    56  	packages = append(packages, &v2_3.Package{
    57  		PackageName:           "main",
    58  		PackageSPDXIdentifier: common.ElementID(mainPackageID),
    59  		PackageVersion:        "0",
    60  		PackageSupplier: &common.Supplier{
    61  			Supplier:     NoAssertion,
    62  			SupplierType: NoAssertion,
    63  		},
    64  		PackageDownloadLocation:   NoAssertion,
    65  		IsFilesAnalyzedTagPresent: false,
    66  	})
    67  
    68  	relationships := make([]*v2_3.Relationship, 0, 1+2*len(r.Inventory.Packages))
    69  	relationships = append(relationships, &v2_3.Relationship{
    70  		RefA:         toDocElementID(SPDXDocumentID),
    71  		RefB:         toDocElementID(mainPackageID),
    72  		Relationship: "DESCRIBES",
    73  	})
    74  
    75  	allOtherLicenses := stringset.Set{}
    76  
    77  	for _, pkg := range r.Inventory.Packages {
    78  		p := pkg.PURL()
    79  		if p == nil {
    80  			log.Warnf("Package %v has no PURL, skipping", pkg)
    81  			continue
    82  		}
    83  		pName := p.Name
    84  		pVersion := p.Version
    85  		if pName == "" || pVersion == "" {
    86  			log.Warnf("Package %v PURL name or version empty, skipping", pkg)
    87  			continue
    88  		}
    89  		pID := SPDXRefPrefix + "Package-" + replaceSPDXIDInvalidChars(pName) + "-" + uuid.New().String()
    90  		pSourceInfo := ""
    91  		if len(pkg.Plugins) > 0 {
    92  			pSourceInfo = fmt.Sprintf("Identified by the %s extractor", pkg.Plugins[0])
    93  		}
    94  		if len(pkg.Locations) == 1 {
    95  			pSourceInfo += " from " + pkg.Locations[0]
    96  		} else if l := len(pkg.Locations); l > 1 {
    97  			pSourceInfo += fmt.Sprintf(" from %d locations, including %s and %s", l, pkg.Locations[0], pkg.Locations[1])
    98  		}
    99  
   100  		licensesConcluded, otherLicenses := LicenseExpression(pkg.Licenses)
   101  		allOtherLicenses.Update(otherLicenses)
   102  
   103  		packages = append(packages, &v2_3.Package{
   104  			PackageName:           pName,
   105  			PackageSPDXIdentifier: common.ElementID(pID),
   106  			PackageVersion:        pVersion,
   107  			PackageSupplier: &common.Supplier{
   108  				Supplier:     NoAssertion,
   109  				SupplierType: NoAssertion,
   110  			},
   111  			PackageDownloadLocation:   NoAssertion,
   112  			PackageLicenseConcluded:   licensesConcluded,
   113  			PackageLicenseDeclared:    NoAssertion,
   114  			IsFilesAnalyzedTagPresent: false,
   115  			PackageSourceInfo:         pSourceInfo,
   116  			PackageExternalReferences: []*v2_3.PackageExternalReference{
   117  				{
   118  					Category: "PACKAGE-MANAGER",
   119  					RefType:  "purl",
   120  					Locator:  p.String(),
   121  				},
   122  			},
   123  		})
   124  		// TODO(b/313658493): Add a DESCRIBES relationship or a DocumentDescribes field.
   125  		relationships = append(relationships, &v2_3.Relationship{
   126  			RefA:         toDocElementID(mainPackageID),
   127  			RefB:         toDocElementID(pID),
   128  			Relationship: "CONTAINS",
   129  		})
   130  		relationships = append(relationships, &v2_3.Relationship{
   131  			RefA:         toDocElementID(pID),
   132  			RefB:         toDocElementID(NoAssertion),
   133  			Relationship: "CONTAINS",
   134  		})
   135  	}
   136  	name := c.DocumentName
   137  	if name == "" {
   138  		name = "SCALIBR-generated SPDX"
   139  	}
   140  	namespace := c.DocumentNamespace
   141  	if namespace == "" {
   142  		namespace = "https://spdx.google/" + uuid.New().String()
   143  	}
   144  	creators := []common.Creator{
   145  		{
   146  			CreatorType: "Tool",
   147  			Creator:     "SCALIBR",
   148  		},
   149  	}
   150  	creators = append(creators, c.Creators...)
   151  	return &v2_3.Document{
   152  		SPDXVersion:       "SPDX-2.3",
   153  		DataLicense:       "CC0-1.0",
   154  		SPDXIdentifier:    "DOCUMENT",
   155  		DocumentName:      name,
   156  		DocumentNamespace: namespace,
   157  		CreationInfo: &v2_3.CreationInfo{
   158  			Creators: creators,
   159  			Created:  time.Now().UTC().Format("2006-01-02T15:04:05Z"),
   160  		},
   161  		Packages:      packages,
   162  		Relationships: relationships,
   163  		OtherLicenses: ToOtherLicenses(allOtherLicenses),
   164  	}
   165  }
   166  
   167  func replaceSPDXIDInvalidChars(id string) string {
   168  	return spdxIDInvalidCharRe.ReplaceAllString(id, "-")
   169  }
   170  
   171  func toDocElementID(id string) common.DocElementID {
   172  	if id == NoAssertion {
   173  		return common.DocElementID{
   174  			SpecialID: NoAssertion,
   175  		}
   176  	}
   177  	return common.DocElementID{
   178  		ElementRefID: common.ElementID(id),
   179  	}
   180  }