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