github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/github/encoder.go (about) 1 package github 2 3 import ( 4 "fmt" 5 "strings" 6 "time" 7 8 "github.com/mholt/archiver/v3" 9 10 "github.com/anchore/packageurl-go" 11 "github.com/anchore/syft/internal/log" 12 "github.com/anchore/syft/syft/pkg" 13 "github.com/anchore/syft/syft/sbom" 14 "github.com/anchore/syft/syft/source" 15 ) 16 17 // toGithubModel converts the provided SBOM to a GitHub dependency model 18 func toGithubModel(s *sbom.SBOM) DependencySnapshot { 19 scanTime := time.Now().Format(time.RFC3339) // TODO is there a record of this somewhere? 20 v := s.Descriptor.Version 21 if v == "[not provided]" || v == "" { 22 v = "0.0.0-dev" 23 } 24 return DependencySnapshot{ 25 Version: 0, 26 // TODO allow property input to specify the Job, Sha, and Ref 27 Detector: DetectorMetadata{ 28 Name: s.Descriptor.Name, 29 URL: "https://github.com/anchore/syft", 30 Version: v, 31 }, 32 Metadata: toSnapshotMetadata(s), 33 Manifests: toGithubManifests(s), 34 Scanned: scanTime, 35 } 36 } 37 38 // toSnapshotMetadata captures the linux distribution information and other metadata 39 func toSnapshotMetadata(s *sbom.SBOM) Metadata { 40 out := Metadata{} 41 42 if s.Artifacts.LinuxDistribution != nil { 43 d := s.Artifacts.LinuxDistribution 44 qualifiers := packageurl.Qualifiers{} 45 if len(d.IDLike) > 0 { 46 qualifiers = append(qualifiers, packageurl.Qualifier{ 47 Key: "like", 48 Value: strings.Join(d.IDLike, ","), 49 }) 50 } 51 purl := packageurl.NewPackageURL("generic", "", d.ID, d.VersionID, qualifiers, "") 52 out["syft:distro"] = purl.ToString() 53 } 54 55 return out 56 } 57 58 func filesystem(p pkg.Package) string { 59 locations := p.Locations.ToSlice() 60 if len(locations) > 0 { 61 return locations[0].FileSystemID 62 } 63 return "" 64 } 65 66 // toGithubManifests manifests, each of which represents a specific location that has dependencies 67 func toGithubManifests(s *sbom.SBOM) Manifests { 68 manifests := map[string]*Manifest{} 69 70 for _, p := range s.Artifacts.Packages.Sorted() { 71 path := toPath(s.Source, p) 72 manifest, ok := manifests[path] 73 if !ok { 74 manifest = &Manifest{ 75 Name: path, 76 File: FileInfo{ 77 SourceLocation: path, 78 }, 79 Resolved: DependencyGraph{}, 80 } 81 fs := filesystem(p) 82 if fs != "" { 83 manifest.Metadata = Metadata{ 84 "syft:filesystem": fs, 85 } 86 } 87 manifests[path] = manifest 88 } 89 90 name := dependencyName(p) 91 manifest.Resolved[name] = DependencyNode{ 92 PackageURL: p.PURL, 93 Metadata: toDependencyMetadata(p), 94 Relationship: toDependencyRelationshipType(p), 95 Scope: toDependencyScope(p), 96 Dependencies: toDependencies(s, p), 97 } 98 } 99 100 out := Manifests{} 101 for k, v := range manifests { 102 out[k] = *v 103 } 104 return out 105 } 106 107 // toPath Generates a string representation of the package location, optionally including the layer hash 108 func toPath(s source.Description, p pkg.Package) string { 109 inputPath := trimRelative(s.Name) 110 locations := p.Locations.ToSlice() 111 if len(locations) > 0 { 112 location := locations[0] 113 packagePath := location.RealPath 114 if location.VirtualPath != "" { 115 packagePath = location.VirtualPath 116 } 117 packagePath = strings.TrimPrefix(packagePath, "/") 118 switch metadata := s.Metadata.(type) { 119 case source.StereoscopeImageSourceMetadata: 120 image := strings.ReplaceAll(metadata.UserInput, ":/", "//") 121 return fmt.Sprintf("%s:/%s", image, packagePath) 122 case source.FileSourceMetadata: 123 path := trimRelative(metadata.Path) 124 if isArchive(metadata.Path) { 125 return fmt.Sprintf("%s:/%s", path, packagePath) 126 } 127 return path 128 case source.DirectorySourceMetadata: 129 path := trimRelative(metadata.Path) 130 if path != "" { 131 return fmt.Sprintf("%s/%s", path, packagePath) 132 } 133 return packagePath 134 } 135 } 136 return inputPath 137 } 138 139 func trimRelative(s string) string { 140 s = strings.TrimPrefix(s, "./") 141 if s == "." { 142 s = "" 143 } 144 return s 145 } 146 147 // isArchive returns true if the path appears to be an archive 148 func isArchive(path string) bool { 149 _, err := archiver.ByExtension(path) 150 return err == nil 151 } 152 153 func toDependencies(s *sbom.SBOM, p pkg.Package) (out []string) { 154 for _, r := range s.Relationships { 155 if r.From.ID() == p.ID() { 156 if p, ok := r.To.(pkg.Package); ok { 157 out = append(out, dependencyName(p)) 158 } 159 } 160 } 161 return 162 } 163 164 // dependencyName to make things a little nicer to read; this might end up being lossy 165 func dependencyName(p pkg.Package) string { 166 purl, err := packageurl.FromString(p.PURL) 167 if err != nil { 168 log.Warnf("Invalid PURL for package: '%s' PURL: '%s' (%w)", p.Name, p.PURL, err) 169 return "" 170 } 171 // don't use qualifiers for this 172 purl.Qualifiers = nil 173 return purl.ToString() 174 } 175 176 func toDependencyScope(_ pkg.Package) DependencyScope { 177 return DependencyScopeRuntime 178 } 179 180 func toDependencyRelationshipType(_ pkg.Package) DependencyRelationship { 181 return DependencyRelationshipDirect 182 } 183 184 func toDependencyMetadata(_ pkg.Package) Metadata { 185 // We have limited properties: up to 8 with reasonably small values 186 // For now, we are encoding the location as part of the key, we are encoding PURLs with most 187 // of the other information Grype might need; and the distro information at the top level 188 // so we don't need anything here yet 189 return Metadata{} 190 }