github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/pkg/package.go (about) 1 /* 2 Package pkg provides the data structures for a package, a package catalog, package types, and domain-specific metadata. 3 */ 4 package pkg 5 6 import ( 7 "fmt" 8 "sort" 9 "strings" 10 11 "github.com/anchore/syft/internal/log" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/cpe" 14 "github.com/anchore/syft/syft/file" 15 ) 16 17 // Package represents an application or library that has been bundled into a distributable format. 18 // TODO: if we ignore FoundBy for ID generation should we merge the field to show it was found in two places? 19 type Package struct { 20 id artifact.ID `hash:"ignore"` 21 Name string // the package name 22 Version string // the version of the package 23 FoundBy string `hash:"ignore" cyclonedx:"foundBy"` // the specific cataloger that discovered this package 24 Locations file.LocationSet // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) 25 Licenses LicenseSet // licenses discovered with the package metadata 26 Language Language `hash:"ignore" cyclonedx:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) 27 Type Type `cyclonedx:"type"` // the package type (e.g. Npm, Yarn, Python, Rpm, Deb, etc) 28 CPEs []cpe.CPE `hash:"ignore"` // all possible Common Platform Enumerators (note: this is NOT included in the definition of the ID since all fields on a CPE are derived from other fields) 29 PURL string `hash:"ignore"` // the Package URL (see https://github.com/package-url/purl-spec) 30 Metadata interface{} // additional data found while parsing the package source 31 } 32 33 func (p *Package) OverrideID(id artifact.ID) { 34 p.id = id 35 } 36 37 func (p *Package) SetID() { 38 id, err := artifact.IDByHash(p) 39 if err != nil { 40 // TODO: what to do in this case? 41 log.Warnf("unable to get fingerprint of package=%s@%s: %+v", p.Name, p.Version, err) 42 return 43 } 44 p.id = id 45 } 46 47 func (p Package) ID() artifact.ID { 48 return p.id 49 } 50 51 // Stringer to represent a package. 52 func (p Package) String() string { 53 return fmt.Sprintf("Pkg(name=%q version=%q type=%q id=%q)", p.Name, p.Version, p.Type, p.id) 54 } 55 56 func (p *Package) merge(other Package) error { 57 if p.id != other.id { 58 return fmt.Errorf("cannot merge packages with different IDs: %q vs %q", p.id, other.id) 59 } 60 61 if p.PURL != other.PURL { 62 log.Warnf("merging packages have with different pURLs: %q=%q vs %q=%q", p.id, p.PURL, other.id, other.PURL) 63 } 64 65 p.Locations.Add(other.Locations.ToSlice()...) 66 p.Licenses.Add(other.Licenses.ToSlice()...) 67 68 p.CPEs = cpe.Merge(p.CPEs, other.CPEs) 69 70 if p.PURL == "" { 71 p.PURL = other.PURL 72 } 73 return nil 74 } 75 76 // IsValid checks whether a package has the minimum necessary info 77 // which is a non-empty name. 78 // The nil-check was added as a helper as often, in this code base, packages 79 // move between callers as pointers. 80 // CycloneDX and SPDX define Name as the minimum required info for a valid package: 81 // * https://spdx.github.io/spdx-spec/package-information/#73-package-version-field 82 // * https://cyclonedx.org/docs/1.4/json/#components_items_name 83 func IsValid(p *Package) bool { 84 return p != nil && p.Name != "" 85 } 86 87 //nolint:gocognit 88 func Less(i, j Package) bool { 89 if i.Name == j.Name { 90 if i.Version == j.Version { 91 iLocations := i.Locations.ToSlice() 92 jLocations := j.Locations.ToSlice() 93 if i.Type == j.Type { 94 maxLen := len(iLocations) 95 if len(jLocations) > maxLen { 96 maxLen = len(jLocations) 97 } 98 for l := 0; l < maxLen; l++ { 99 if len(iLocations) < l+1 || len(jLocations) < l+1 { 100 if len(iLocations) == len(jLocations) { 101 break 102 } 103 return len(iLocations) < len(jLocations) 104 } 105 if iLocations[l].RealPath == jLocations[l].RealPath { 106 continue 107 } 108 return iLocations[l].RealPath < jLocations[l].RealPath 109 } 110 // compare remaining metadata as a final fallback 111 // note: we cannot guarantee that IDs (which digests the metadata) are stable enough to sort on 112 // when there are potentially missing elements there is too much reduction in the dimensions to 113 // lean on ID comparison. The best fallback is to look at the string representation of the metadata. 114 return strings.Compare(fmt.Sprintf("%#v", i.Metadata), fmt.Sprintf("%#v", j.Metadata)) < 0 115 } 116 return i.Type < j.Type 117 } 118 return i.Version < j.Version 119 } 120 return i.Name < j.Name 121 } 122 func Sort(pkgs []Package) { 123 sort.SliceStable(pkgs, func(i, j int) bool { 124 return Less(pkgs[i], pkgs[j]) 125 }) 126 }