mirror of
https://github.com/anchore/syft.git
synced 2026-11-04 00:16:38 +01:00
Decoding an SPDX document into the syft model was quadratic in document
size. findRootPackages (spdxhelpers/to_syft_model.go), which locates the
packages the document DESCRIBES, scanned every relationship for every
package. On a 119MB SPDX JSON document (70,701 packages, 153,301
relationships) that is 1.1e10 comparisons and 91% of all CPU samples; on a
238MB document it is 4.3e10 comparisons and 95%. A 1.3GB document did not
finish in 91 minutes. Everything else on the decode path is map-indexed and
linear, and spdx/tools-golang's own parse is linear (4.5s and 9.3s for the
two documents above). The scan dates from 9480f10c (top-level SPDX package
support, 2023).
The function now indexes the DESCRIBES / DESCRIBED_BY relationships once,
counting how many times each element is described by the DOCUMENT, then
walks the packages once. Semantics are unchanged: results follow package
order, an element described by more than one edge still appears once per
edge (so the existing "exactly one root" check behaves as before), only
edges touching DOCUMENT count, and nil entries are skipped.
Measured on an Apple M1 Max, `syft convert <spdx> -o <format>=<file>`:
input target before (main) after
119MB spdx-json@2.3 68.2s / 1.3GB 10.4s / 1.3GB
119MB spdx-json@2.2 70.2s / 1.3GB 12.6s / 1.3GB
238MB spdx-json@2.3 291.2s / 2.3GB 20.9s / 2.6GB
1.3GB spdx-json@2.3 killed at 91min 115.5s / 13.6GB
1.3GB spdx-json@2.2 not attempted 136.3s / 14.1GB
1.3GB syft-json not attempted 102.6s / 14.1GB
1.3GB cyclonedx-json not attempted 114.1s / 13.1GB
Isolating the conversion step (ToSyftModel) from the tools-golang parse:
119MB 72.3s -> 1.5s, 238MB 265.8s -> 3.1s. The remaining decode time and
all of the memory are in tools-golang, which parses the document several
times over; that is a separate, upstream concern.
Output equivalence was checked by converting 10 SPDX documents (the 8 SPDX
fixtures in this repository plus the 119MB and 238MB documents) to
syft-json, spdx-json@2.3, spdx-json@2.2, spdx-tag-value and
cyclonedx-json@1.6 with both binaries and comparing the results with
timestamps and generated ids removed: all 50 pairs are identical.
Tests pin the original semantics (including the duplicate-edge corner case)
and guard the complexity with a 50k x 100k document that must complete in
under 10s; the old implementation takes about a minute on it.
Signed-off-by: juanjsebgarcia <github@juangarcia.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
704 lines
18 KiB
Go
704 lines
18 KiB
Go
package spdxhelpers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/scylladb/go-set/strset"
|
|
"github.com/spdx/tools-golang/spdx"
|
|
"github.com/spdx/tools-golang/spdx/v2/common"
|
|
|
|
"github.com/anchore/packageurl-go"
|
|
"github.com/anchore/syft/internal/log"
|
|
"github.com/anchore/syft/internal/spdxlicense"
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/cpe"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/format/internal"
|
|
"github.com/anchore/syft/syft/format/internal/spdxutil/helpers"
|
|
"github.com/anchore/syft/syft/license"
|
|
"github.com/anchore/syft/syft/linux"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
"github.com/anchore/syft/syft/sbom"
|
|
"github.com/anchore/syft/syft/source"
|
|
)
|
|
|
|
func ToSyftModel(doc *spdx.Document) (*sbom.SBOM, error) {
|
|
if doc == nil {
|
|
return nil, errors.New("cannot convert SPDX document to Syft model because document is nil")
|
|
}
|
|
|
|
spdxIDMap := make(map[string]any)
|
|
|
|
s := &sbom.SBOM{
|
|
Source: extractSource(spdxIDMap, doc),
|
|
Artifacts: sbom.Artifacts{
|
|
Packages: pkg.NewCollection(),
|
|
FileMetadata: map[file.Coordinates]file.Metadata{},
|
|
FileDigests: map[file.Coordinates][]file.Digest{},
|
|
LinuxDistribution: findLinuxReleaseByPURL(doc),
|
|
},
|
|
}
|
|
|
|
collectSyftPackages(s, spdxIDMap, doc)
|
|
|
|
collectSyftFiles(s, spdxIDMap, doc)
|
|
|
|
s.Relationships = toSyftRelationships(spdxIDMap, doc)
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func isDirectory(name string) bool {
|
|
if name == "." || name == ".." || strings.HasSuffix(name, "/") || !strings.Contains(path.Base(name), ".") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func removePackage(packages []*spdx.Package, remove *spdx.Package) (pkgs []*spdx.Package) {
|
|
for _, p := range packages {
|
|
if p == remove {
|
|
continue
|
|
}
|
|
pkgs = append(pkgs, p)
|
|
}
|
|
return
|
|
}
|
|
|
|
func removeRelationships(relationships []*spdx.Relationship, spdxID spdx.ElementID) (relations []*spdx.Relationship) {
|
|
for _, r := range relationships {
|
|
if r.RefA.ElementRefID == spdxID || r.RefB.ElementRefID == spdxID {
|
|
continue
|
|
}
|
|
relations = append(relations, r)
|
|
}
|
|
return
|
|
}
|
|
|
|
func findRootPackages(doc *spdx.Document) (out []*spdx.Package) {
|
|
// index the DESCRIBES / DESCRIBED_BY relationships once, counting how many
|
|
// times each element is described by the document, so that the result
|
|
// matches the previous packages x relationships scan exactly (one entry per
|
|
// matching relationship) without the quadratic cost.
|
|
describedCount := make(map[spdx.ElementID]int)
|
|
for _, r := range doc.Relationships {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
switch {
|
|
case r.RefA.ElementRefID == "DOCUMENT" && r.Relationship == spdx.RelationshipDescribes:
|
|
describedCount[r.RefB.ElementRefID]++
|
|
case r.RefB.ElementRefID == "DOCUMENT" && r.Relationship == spdx.RelationshipDescribedBy:
|
|
describedCount[r.RefA.ElementRefID]++
|
|
}
|
|
}
|
|
if len(describedCount) == 0 {
|
|
return nil
|
|
}
|
|
for _, p := range doc.Packages {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
for i := 0; i < describedCount[p.PackageSPDXIdentifier]; i++ {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func extractSource(spdxIDMap map[string]any, doc *spdx.Document) source.Description {
|
|
src := extractSourceFromNamespace(doc.DocumentNamespace)
|
|
|
|
rootPackages := findRootPackages(doc)
|
|
|
|
if len(rootPackages) != 1 {
|
|
return src
|
|
}
|
|
|
|
p := rootPackages[0]
|
|
|
|
switch p.PrimaryPackagePurpose {
|
|
case spdxPrimaryPurposeContainer:
|
|
src = containerSource(p)
|
|
case spdxPrimaryPurposeFile:
|
|
src = fileSource(p)
|
|
default:
|
|
return src
|
|
}
|
|
|
|
spdxIDMap[string(p.PackageSPDXIdentifier)] = src
|
|
|
|
doc.Packages = removePackage(doc.Packages, p)
|
|
doc.Relationships = removeRelationships(doc.Relationships, p.PackageSPDXIdentifier)
|
|
|
|
return src
|
|
}
|
|
|
|
func containerSource(p *spdx.Package) source.Description {
|
|
id := string(p.PackageSPDXIdentifier)
|
|
|
|
container := p.PackageName
|
|
v := p.PackageVersion
|
|
if v != "" {
|
|
container += ":" + v
|
|
}
|
|
|
|
digest := ""
|
|
if len(p.PackageChecksums) > 0 {
|
|
c := p.PackageChecksums[0]
|
|
digest = fmt.Sprintf("%s:%s", fromChecksumAlgorithm(c.Algorithm), c.Value)
|
|
}
|
|
|
|
supplier := ""
|
|
if p.PackageSupplier != nil {
|
|
// we also don't want NOASSERTION transferred to the syft format
|
|
// NOASSERTION == ""
|
|
if p.PackageSupplier.Supplier != helpers.NOASSERTION && p.PackageSupplier.SupplierType == helpers.SUPPLIERORG {
|
|
supplier = p.PackageSupplier.Supplier
|
|
}
|
|
}
|
|
|
|
return source.Description{
|
|
ID: id,
|
|
Name: p.PackageName,
|
|
Version: p.PackageVersion,
|
|
Supplier: supplier,
|
|
Metadata: source.ImageMetadata{
|
|
UserInput: container,
|
|
ID: id,
|
|
Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool
|
|
ManifestDigest: digest,
|
|
},
|
|
}
|
|
}
|
|
|
|
func fileSource(p *spdx.Package) source.Description {
|
|
typeRegex := regexp.MustCompile("^DocumentRoot-([^-]+)-.*$")
|
|
typeName := typeRegex.ReplaceAllString(string(p.PackageSPDXIdentifier), "$1")
|
|
|
|
var version string
|
|
var metadata any
|
|
switch {
|
|
case typeName == prefixDirectory:
|
|
// is a Syft SBOM, explicitly a directory source
|
|
metadata, version = directorySourceMetadata(p)
|
|
case typeName == prefixFile:
|
|
// is a Syft SBOM, explicitly a file source
|
|
metadata, version = fileSourceMetadata(p)
|
|
case isDirectory(p.PackageName):
|
|
// is a non-Syft SBOM, which looks like a directory
|
|
metadata, version = directorySourceMetadata(p)
|
|
default:
|
|
// is a non-Syft SBOM, which is probably a file
|
|
metadata, version = fileSourceMetadata(p)
|
|
}
|
|
|
|
supplier := ""
|
|
if p.PackageSupplier.Supplier != helpers.NOASSERTION {
|
|
supplier = p.PackageSupplier.Supplier
|
|
}
|
|
|
|
return source.Description{
|
|
ID: string(p.PackageSPDXIdentifier),
|
|
Name: p.PackageName,
|
|
Version: version,
|
|
Supplier: supplier,
|
|
Metadata: metadata,
|
|
}
|
|
}
|
|
|
|
func fileSourceMetadata(p *spdx.Package) (any, string) {
|
|
version := p.PackageVersion
|
|
|
|
m := source.FileMetadata{
|
|
Path: p.PackageName,
|
|
}
|
|
// if this is a Syft SBOM, we might have output a digest as the version
|
|
checksum := toChecksum(p.PackageVersion)
|
|
for _, d := range p.PackageChecksums {
|
|
if checksum != nil && checksum.Value == d.Value {
|
|
version = ""
|
|
}
|
|
m.Digests = append(m.Digests, file.Digest{
|
|
Algorithm: fromChecksumAlgorithm(d.Algorithm),
|
|
Value: d.Value,
|
|
})
|
|
}
|
|
|
|
return m, version
|
|
}
|
|
|
|
func directorySourceMetadata(p *spdx.Package) (any, string) {
|
|
return source.DirectoryMetadata{
|
|
Path: p.PackageName,
|
|
Base: "",
|
|
}, p.PackageVersion
|
|
}
|
|
|
|
// NOTE(jonas): SPDX doesn't inform what an SBOM is about,
|
|
// image, directory, for example. This is our best effort to determine
|
|
// the scheme. Syft-generated SBOMs have in the namespace
|
|
// field a type encoded, which we try to identify here.
|
|
func extractSourceFromNamespace(ns string) source.Description {
|
|
u, err := url.Parse(ns)
|
|
if err != nil {
|
|
return source.Description{
|
|
Metadata: nil,
|
|
}
|
|
}
|
|
|
|
parts := strings.Split(u.Path, "/")
|
|
for _, p := range parts {
|
|
switch p {
|
|
case helpers.InputFile:
|
|
return source.Description{
|
|
Metadata: source.FileMetadata{},
|
|
}
|
|
case helpers.InputImage:
|
|
return source.Description{
|
|
Metadata: source.ImageMetadata{},
|
|
}
|
|
case helpers.InputDirectory:
|
|
return source.Description{
|
|
Metadata: source.DirectoryMetadata{},
|
|
}
|
|
}
|
|
}
|
|
return source.Description{}
|
|
}
|
|
|
|
func findLinuxReleaseByPURL(doc *spdx.Document) *linux.Release {
|
|
for _, p := range doc.Packages {
|
|
purlValue := findPURLValue(p)
|
|
if purlValue == "" {
|
|
continue
|
|
}
|
|
purl, err := packageurl.FromString(purlValue)
|
|
if err != nil {
|
|
log.Warnf("unable to parse purl: %s", purlValue)
|
|
continue
|
|
}
|
|
distro := findQualifierValue(purl, pkg.PURLQualifierDistro)
|
|
if distro != "" {
|
|
parts := strings.Split(distro, "-")
|
|
name := parts[0]
|
|
version := ""
|
|
if len(parts) > 1 {
|
|
version = parts[1]
|
|
}
|
|
return &linux.Release{
|
|
PrettyName: name,
|
|
Name: name,
|
|
ID: name,
|
|
IDLike: []string{name},
|
|
Version: version,
|
|
VersionID: version,
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectSyftPackages(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) {
|
|
skipIDs := packageIDsToSkip(doc)
|
|
for _, p := range doc.Packages {
|
|
if p == nil || skipIDs.Has(string(p.PackageSPDXIdentifier)) {
|
|
continue
|
|
}
|
|
syftPkg := toSyftPackage(p)
|
|
spdxIDMap[string(p.PackageSPDXIdentifier)] = syftPkg
|
|
s.Artifacts.Packages.Add(syftPkg)
|
|
}
|
|
}
|
|
|
|
func collectSyftFiles(s *sbom.SBOM, spdxIDMap map[string]any, doc *spdx.Document) {
|
|
for _, p := range doc.Packages {
|
|
for _, f := range p.Files {
|
|
l := toSyftLocation(f)
|
|
spdxIDMap[string(f.FileSPDXIdentifier)] = l
|
|
|
|
s.Artifacts.FileMetadata[l.Coordinates] = toFileMetadata(f)
|
|
s.Artifacts.FileDigests[l.Coordinates] = toFileDigests(f)
|
|
}
|
|
}
|
|
|
|
for _, f := range doc.Files {
|
|
l := toSyftLocation(f)
|
|
spdxIDMap[string(f.FileSPDXIdentifier)] = l
|
|
|
|
s.Artifacts.FileMetadata[l.Coordinates] = toFileMetadata(f)
|
|
s.Artifacts.FileDigests[l.Coordinates] = toFileDigests(f)
|
|
}
|
|
}
|
|
|
|
func toFileDigests(f *spdx.File) (digests []file.Digest) {
|
|
for _, digest := range f.Checksums {
|
|
digests = append(digests, file.Digest{
|
|
Algorithm: fromChecksumAlgorithm(digest.Algorithm),
|
|
Value: digest.Value,
|
|
})
|
|
}
|
|
return digests
|
|
}
|
|
|
|
func fromChecksumAlgorithm(algorithm common.ChecksumAlgorithm) string {
|
|
return strings.ToLower(string(algorithm))
|
|
}
|
|
|
|
func toFileMetadata(f *spdx.File) (meta file.Metadata) {
|
|
// FIXME Syft is currently lossy due to the SPDX 2.2.1 spec not supporting arbitrary mimetypes
|
|
for _, typ := range f.FileTypes {
|
|
switch helpers.FileType(typ) {
|
|
case helpers.ImageFileType:
|
|
meta.MIMEType = "image/"
|
|
case helpers.VideoFileType:
|
|
meta.MIMEType = "video/"
|
|
case helpers.ApplicationFileType:
|
|
meta.MIMEType = "application/"
|
|
case helpers.TextFileType:
|
|
meta.MIMEType = "text/"
|
|
case helpers.AudioFileType:
|
|
meta.MIMEType = "audio/"
|
|
case helpers.BinaryFileType:
|
|
case helpers.ArchiveFileType:
|
|
case helpers.OtherFileType:
|
|
}
|
|
}
|
|
return meta
|
|
}
|
|
|
|
func toSyftRelationships(spdxIDMap map[string]any, doc *spdx.Document) []artifact.Relationship {
|
|
out := collectDocRelationships(spdxIDMap, doc)
|
|
|
|
out = append(out, collectPackageFileRelationships(spdxIDMap, doc)...)
|
|
|
|
return out
|
|
}
|
|
|
|
func collectDocRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out []artifact.Relationship) {
|
|
for _, r := range doc.Relationships {
|
|
// FIXME what to do with r.RefA.DocumentRefID and r.RefA.SpecialID
|
|
if r.RefA.DocumentRefID != "" && requireAndTrimPrefix(r.RefA.DocumentRefID, "DocumentRef-") != string(doc.SPDXIdentifier) {
|
|
log.Debugf("ignoring relationship to external document: %+v", r)
|
|
continue
|
|
}
|
|
a := spdxIDMap[string(r.RefA.ElementRefID)]
|
|
b := spdxIDMap[string(r.RefB.ElementRefID)]
|
|
from, fromOk := a.(pkg.Package)
|
|
toPackage, toPackageOk := b.(pkg.Package)
|
|
toLocation, toLocationOk := b.(file.Location)
|
|
//nolint:staticcheck
|
|
if !fromOk || !(toPackageOk || toLocationOk) {
|
|
log.Debugf("unable to find valid relationship mapping from SPDX, ignoring: (from: %+v) (to: %+v)", a, b)
|
|
continue
|
|
}
|
|
var to artifact.Identifiable
|
|
var typ artifact.RelationshipType
|
|
if toLocationOk {
|
|
switch helpers.RelationshipType(r.Relationship) {
|
|
case helpers.ContainsRelationship:
|
|
typ = artifact.ContainsRelationship
|
|
to = toLocation
|
|
case helpers.OtherRelationship:
|
|
// Encoding uses a specifically formatted comment...
|
|
if strings.HasPrefix(r.RelationshipComment, string(artifact.EvidentByRelationship)) {
|
|
typ = artifact.EvidentByRelationship
|
|
to = toLocation
|
|
}
|
|
}
|
|
} else {
|
|
switch helpers.RelationshipType(r.Relationship) {
|
|
case helpers.DependencyOfRelationship:
|
|
typ = artifact.DependencyOfRelationship
|
|
to = toPackage
|
|
case helpers.DependsOnRelationship:
|
|
typ = artifact.DependencyOfRelationship
|
|
to = from
|
|
from = toPackage
|
|
case helpers.ContainsRelationship:
|
|
typ = artifact.ContainsRelationship
|
|
to = toPackage
|
|
case helpers.OtherRelationship:
|
|
// Encoding uses a specifically formatted comment...
|
|
if strings.HasPrefix(r.RelationshipComment, string(artifact.OwnershipByFileOverlapRelationship)) {
|
|
typ = artifact.OwnershipByFileOverlapRelationship
|
|
to = toPackage
|
|
}
|
|
}
|
|
}
|
|
if typ != "" && to != nil {
|
|
out = append(out, artifact.Relationship{
|
|
From: from,
|
|
To: to,
|
|
Type: typ,
|
|
})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// collectPackageFileRelationships add relationships for direct files
|
|
func collectPackageFileRelationships(spdxIDMap map[string]any, doc *spdx.Document) (out []artifact.Relationship) {
|
|
for _, p := range doc.Packages {
|
|
a := spdxIDMap[string(p.PackageSPDXIdentifier)]
|
|
from, fromOk := a.(pkg.Package)
|
|
if !fromOk {
|
|
continue
|
|
}
|
|
for _, f := range p.Files {
|
|
b := spdxIDMap[string(f.FileSPDXIdentifier)]
|
|
to, toLocationOk := b.(file.Location)
|
|
if !toLocationOk {
|
|
continue
|
|
}
|
|
out = append(out, artifact.Relationship{
|
|
From: from,
|
|
To: to,
|
|
Type: artifact.ContainsRelationship,
|
|
})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func toSyftCoordinates(f *spdx.File) file.Coordinates {
|
|
const layerIDPrefix = "layerID: "
|
|
var fileSystemID string
|
|
if strings.HasPrefix(f.FileComment, layerIDPrefix) {
|
|
fileSystemID = strings.TrimPrefix(f.FileComment, layerIDPrefix)
|
|
}
|
|
if strings.HasPrefix(string(f.FileSPDXIdentifier), layerIDPrefix) {
|
|
fileSystemID = strings.TrimPrefix(string(f.FileSPDXIdentifier), layerIDPrefix)
|
|
}
|
|
return file.Coordinates{
|
|
RealPath: f.FileName,
|
|
FileSystemID: fileSystemID,
|
|
}
|
|
}
|
|
|
|
func toSyftLocation(f *spdx.File) file.Location {
|
|
l := file.NewVirtualLocationFromCoordinates(toSyftCoordinates(f), f.FileName)
|
|
return l
|
|
}
|
|
|
|
func requireAndTrimPrefix(val any, prefix string) string {
|
|
if v, ok := val.(string); ok {
|
|
if strings.HasPrefix(v, prefix) {
|
|
return strings.TrimPrefix(v, prefix)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type pkgInfo struct {
|
|
purl packageurl.PackageURL
|
|
typ pkg.Type
|
|
lang pkg.Language
|
|
}
|
|
|
|
func (p *pkgInfo) qualifierValue(name string) string {
|
|
return findQualifierValue(p.purl, name)
|
|
}
|
|
|
|
func findQualifierValue(purl packageurl.PackageURL, qualifier string) string {
|
|
for _, q := range purl.Qualifiers {
|
|
if q.Key == qualifier {
|
|
return q.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func extractPkgInfo(p *spdx.Package) pkgInfo {
|
|
pu := findPURLValue(p)
|
|
purl, err := packageurl.FromString(pu)
|
|
if err != nil {
|
|
return pkgInfo{}
|
|
}
|
|
return pkgInfo{
|
|
purl,
|
|
pkg.TypeByName(purl.Type),
|
|
pkg.LanguageByName(purl.Type),
|
|
}
|
|
}
|
|
|
|
func toSyftPackage(p *spdx.Package) pkg.Package {
|
|
info := extractPkgInfo(p)
|
|
sP := &pkg.Package{
|
|
Type: info.typ,
|
|
Name: p.PackageName,
|
|
Version: p.PackageVersion,
|
|
Licenses: pkg.NewLicenseSet(parseSPDXLicenses(p)...),
|
|
CPEs: extractCPEs(p),
|
|
PURL: purlValue(info.purl),
|
|
Language: info.lang,
|
|
Metadata: extractMetadata(p, info),
|
|
}
|
|
|
|
internal.Backfill(sP)
|
|
|
|
if p.PackageSPDXIdentifier != "" {
|
|
// always prefer the IDs from the SBOM over derived IDs
|
|
sP.OverrideID(artifact.ID(p.PackageSPDXIdentifier))
|
|
} else {
|
|
sP.SetID()
|
|
}
|
|
|
|
return *sP
|
|
}
|
|
|
|
func purlValue(purl packageurl.PackageURL) string {
|
|
val := purl.String()
|
|
if _, err := packageurl.FromString(val); err != nil {
|
|
return ""
|
|
}
|
|
return val
|
|
}
|
|
|
|
func parseSPDXLicenses(p *spdx.Package) []pkg.License {
|
|
licenses := make([]pkg.License, 0)
|
|
|
|
// concluded
|
|
if p.PackageLicenseConcluded != helpers.NOASSERTION && p.PackageLicenseConcluded != helpers.NONE && p.PackageLicenseConcluded != "" {
|
|
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseConcluded))
|
|
l.Type = license.Concluded
|
|
licenses = append(licenses, l)
|
|
}
|
|
|
|
// declared
|
|
if p.PackageLicenseDeclared != helpers.NOASSERTION && p.PackageLicenseDeclared != helpers.NONE && p.PackageLicenseDeclared != "" {
|
|
l := pkg.NewLicenseWithContext(context.TODO(), cleanSPDXID(p.PackageLicenseDeclared))
|
|
l.Type = license.Declared
|
|
licenses = append(licenses, l)
|
|
}
|
|
|
|
return licenses
|
|
}
|
|
|
|
func cleanSPDXID(id string) string {
|
|
return strings.TrimPrefix(id, spdxlicense.LicenseRefPrefix)
|
|
}
|
|
|
|
//nolint:funlen
|
|
func extractMetadata(p *spdx.Package, info pkgInfo) any {
|
|
arch := info.qualifierValue(pkg.PURLQualifierArch)
|
|
upstreamValue := info.qualifierValue(pkg.PURLQualifierUpstream)
|
|
upstream := strings.SplitN(upstreamValue, "@", 2)
|
|
upstreamName := upstream[0]
|
|
upstreamVersion := ""
|
|
if len(upstream) > 1 {
|
|
upstreamVersion = upstream[1]
|
|
}
|
|
supplier := ""
|
|
if p.PackageSupplier != nil {
|
|
supplier = p.PackageSupplier.Supplier
|
|
}
|
|
originator := ""
|
|
if p.PackageOriginator != nil {
|
|
originator = p.PackageOriginator.Originator
|
|
}
|
|
switch info.typ {
|
|
case pkg.ApkPkg:
|
|
return pkg.ApkDBEntry{
|
|
Package: p.PackageName,
|
|
OriginPackage: upstreamName,
|
|
Maintainer: supplier,
|
|
Version: p.PackageVersion,
|
|
Architecture: arch,
|
|
URL: p.PackageHomePage,
|
|
Description: p.PackageDescription,
|
|
}
|
|
case pkg.RpmPkg:
|
|
converted, err := strconv.Atoi(info.qualifierValue(pkg.PURLQualifierEpoch))
|
|
var epoch *int
|
|
if err != nil {
|
|
epoch = nil
|
|
} else {
|
|
epoch = &converted
|
|
}
|
|
return pkg.RpmDBEntry{
|
|
Name: p.PackageName,
|
|
Version: p.PackageVersion,
|
|
Epoch: epoch,
|
|
Arch: arch,
|
|
SourceRpm: upstreamValue,
|
|
Vendor: originator,
|
|
}
|
|
case pkg.DebPkg:
|
|
return pkg.DpkgDBEntry{
|
|
Package: p.PackageName,
|
|
Source: upstreamName,
|
|
Version: p.PackageVersion,
|
|
SourceVersion: upstreamVersion,
|
|
Architecture: arch,
|
|
Maintainer: originator,
|
|
}
|
|
case pkg.JavaPkg:
|
|
var digests []file.Digest
|
|
for _, value := range p.PackageChecksums {
|
|
digests = append(digests, file.Digest{Algorithm: fromChecksumAlgorithm(value.Algorithm), Value: value.Value})
|
|
}
|
|
return pkg.JavaArchive{
|
|
ArchiveDigests: digests,
|
|
}
|
|
case pkg.GoModulePkg:
|
|
var h1Digest string
|
|
for _, value := range p.PackageChecksums {
|
|
digest, err := helpers.HDigestFromSHA(fromChecksumAlgorithm(value.Algorithm), value.Value)
|
|
if err != nil {
|
|
log.Debugf("invalid h1digest: %v %v", value, err)
|
|
continue
|
|
}
|
|
h1Digest = digest
|
|
break
|
|
}
|
|
return pkg.GolangBinaryBuildinfoEntry{
|
|
H1Digest: h1Digest,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findPURLValue(p *spdx.Package) string {
|
|
for _, r := range p.PackageExternalReferences {
|
|
if r.RefType == string(helpers.PurlExternalRefType) {
|
|
return r.Locator
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
|
|
for _, r := range p.PackageExternalReferences {
|
|
if r.RefType == string(helpers.Cpe23ExternalRefType) {
|
|
c, err := cpe.New(r.Locator, cpe.DeclaredSource)
|
|
if err != nil {
|
|
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err)
|
|
continue
|
|
}
|
|
cpes = append(cpes, c)
|
|
}
|
|
}
|
|
return cpes
|
|
}
|
|
|
|
// packageIDsToSkip returns a set of packageIDs that should not be imported
|
|
func packageIDsToSkip(doc *spdx.Document) *strset.Set {
|
|
skipIDs := strset.New()
|
|
for i := 0; i < len(doc.Relationships); i++ {
|
|
r := doc.Relationships[i]
|
|
if r != nil && r.Relationship == spdx.RelationshipGeneratedFrom {
|
|
skipIDs.Add(string(r.RefB.ElementRefID))
|
|
}
|
|
}
|
|
return skipIDs
|
|
}
|