github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/bitnami/cataloger.go (about) 1 /* 2 Package bitnami provides a concrete Cataloger implementation for capturing packages embedded within Bitnami SBOM files. 3 */ 4 package bitnami 5 6 import ( 7 "context" 8 "path/filepath" 9 "strings" 10 11 "github.com/anchore/syft/internal/log" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/format" 15 "github.com/anchore/syft/syft/pkg" 16 "github.com/anchore/syft/syft/pkg/cataloger/generic" 17 ) 18 19 const catalogerName = "bitnami-cataloger" 20 21 // NewCataloger returns a new SBOM cataloger object loaded from saved SBOM JSON. 22 func NewCataloger() pkg.Cataloger { 23 return generic.NewCataloger(catalogerName). 24 WithParserByGlobs(parseSBOM, 25 "/opt/bitnami/**/.spdx-*.spdx", 26 ) 27 } 28 29 func parseSBOM(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 30 s, sFormat, _, err := format.Decode(reader) 31 if err != nil { 32 return nil, nil, err 33 } 34 35 if s == nil { 36 log.WithFields("path", reader.RealPath).Trace("file is not an SBOM") 37 return nil, nil, nil 38 } 39 40 // Bitnami exclusively uses SPDX JSON SBOMs 41 if sFormat != "spdx-json" { 42 log.WithFields("path", reader.RealPath).Trace("file is not an SPDX JSON SBOM") 43 return nil, nil, nil 44 } 45 46 var pkgs []pkg.Package 47 var secondaryPkgsFiles []string 48 mainPkgID := findMainPkgID(s.Relationships) 49 for _, p := range s.Artifacts.Packages.Sorted() { 50 // We only want to report Bitnami packages 51 if !strings.HasPrefix(p.PURL, "pkg:bitnami") { 52 continue 53 } 54 55 p.FoundBy = catalogerName 56 p.Type = pkg.BitnamiPkg 57 // replace all locations on the package with the location of the SBOM file. 58 // Why not keep the original list of locations? Since the "locations" field is meant to capture 59 // where there is evidence of this file, and the catalogers have not run against any file other than, 60 // the SBOM, this is the only location that is relevant for this cataloger. 61 p.Locations = file.NewLocationSet( 62 reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 63 ) 64 65 // Parse the Bitnami-specific metadata 66 metadata, err := parseBitnamiPURL(p.PURL) 67 if err != nil { 68 return nil, nil, err 69 } 70 71 // Bitnami packages reported in a SPDX file are shipped under the same directory 72 // as the SPDX file itself. 73 metadata.Path = filepath.Dir(reader.RealPath) 74 if p.ID() != mainPkgID { 75 metadata.Files = packageFiles(s.Relationships, p, metadata.Path) 76 secondaryPkgsFiles = append(secondaryPkgsFiles, metadata.Files...) 77 } 78 79 p.Metadata = metadata 80 81 pkgs = append(pkgs, p) 82 } 83 // If there is exactly one package, assume it is the main package 84 if len(pkgs) == 1 && mainPkgID == "" { 85 mainPkgID = pkgs[0].ID() 86 } 87 88 // Resolve all files owned by the main package in the SBOM and update the metadata 89 if mainPkgFiles, err := mainPkgFiles(resolver, reader.RealPath, secondaryPkgsFiles); err == nil { 90 for i, p := range pkgs { 91 if p.ID() == mainPkgID { 92 metadata, ok := p.Metadata.(*pkg.BitnamiSBOMEntry) 93 if !ok { 94 log.WithFields("spdx-filepath", reader.RealPath).Trace("main package in SBOM does not have Bitnami metadata") 95 continue 96 } 97 98 metadata.Files = mainPkgFiles 99 pkgs[i].Metadata = metadata 100 } 101 } 102 } else { 103 log.WithFields("spdx-filepath", reader.RealPath, "error", err).Trace("unable to resolve owned files for main package in SBOM") 104 } 105 106 return pkgs, filterRelationships(s.Relationships, pkgs), nil 107 } 108 109 // filterRelationships filters out relationships that are not related to Bitnami packages 110 // and replaces the package information with the one with completed info 111 func filterRelationships(relationships []artifact.Relationship, pkgs []pkg.Package) []artifact.Relationship { 112 var result []artifact.Relationship 113 for _, r := range relationships { 114 if value, ok := r.From.(pkg.Package); ok { 115 found := false 116 for _, p := range pkgs { 117 if value.PURL == p.PURL { 118 r.From = p 119 found = true 120 break 121 } 122 } 123 if !found { 124 continue 125 } 126 } 127 if value, ok := r.To.(pkg.Package); ok { 128 found := false 129 for _, p := range pkgs { 130 if value.PURL == p.PURL { 131 r.To = p 132 found = true 133 break 134 } 135 } 136 if !found { 137 continue 138 } 139 } 140 result = append(result, r) 141 } 142 143 return result 144 } 145 146 // findMainPkgID goes through the list of relationships and finds the main package ID 147 // which is the one that contains other packages but is not contained by any other package 148 func findMainPkgID(relationships []artifact.Relationship) artifact.ID { 149 containedByAnother := func(candidateID artifact.ID) bool { 150 for _, r := range relationships { 151 if r.Type != artifact.ContainsRelationship { 152 continue 153 } 154 155 if to, ok := r.To.(pkg.Package); ok { 156 if to.ID() == candidateID { 157 return true 158 } 159 } 160 } 161 162 return false 163 } 164 165 for _, r := range relationships { 166 if from, ok := r.From.(pkg.Package); ok { 167 if !strings.HasPrefix(from.PURL, "pkg:bitnami") { 168 continue 169 } 170 if !containedByAnother(from.ID()) { 171 return from.ID() 172 } 173 } 174 } 175 176 return "" 177 }