github.com/anchore/syft@v1.38.2/syft/format/github/internal/model/model.go (about) 1 package model 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/anchore/packageurl-go" 10 "github.com/anchore/syft/internal/file" 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 if name == "" || p.PURL == "" { 92 continue 93 } 94 manifest.Resolved[name] = DependencyNode{ 95 PackageURL: p.PURL, 96 Metadata: toDependencyMetadata(p), 97 Relationship: toDependencyRelationshipType(p), 98 Scope: toDependencyScope(p), 99 Dependencies: toDependencies(s, p), 100 } 101 } 102 103 out := Manifests{} 104 for k, v := range manifests { 105 out[k] = *v 106 } 107 return out 108 } 109 110 // toPath Generates a string representation of the package location, optionally including the layer hash 111 func toPath(s source.Description, p pkg.Package) string { 112 inputPath := trimRelative(s.Name) 113 locations := p.Locations.ToSlice() 114 if len(locations) > 0 { 115 location := locations[0] 116 packagePath := location.RealPath 117 if location.AccessPath != "" { 118 packagePath = location.AccessPath 119 } 120 packagePath = strings.TrimPrefix(packagePath, "/") 121 switch metadata := s.Metadata.(type) { 122 case source.ImageMetadata: 123 image := strings.ReplaceAll(metadata.UserInput, ":/", "//") 124 return fmt.Sprintf("%s:/%s", image, packagePath) 125 case source.FileMetadata: 126 path := trimRelative(metadata.Path) 127 if isArchive(metadata.Path) { 128 return fmt.Sprintf("%s:/%s", path, packagePath) 129 } 130 return path 131 case source.DirectoryMetadata: 132 path := trimRelative(metadata.Path) 133 if path != "" { 134 return fmt.Sprintf("%s/%s", path, packagePath) 135 } 136 return packagePath 137 case source.SnapMetadata: 138 if inputPath != "" { 139 return fmt.Sprintf("%s:/%s", inputPath, packagePath) 140 } 141 return packagePath 142 } 143 } 144 return inputPath 145 } 146 147 func trimRelative(s string) string { 148 s = strings.TrimPrefix(s, "./") 149 if s == "." { 150 s = "" 151 } 152 return s 153 } 154 155 // isArchive returns true if the path appears to be an archive 156 func isArchive(path string) bool { 157 format, _, err := file.IdentifyArchive(context.Background(), path, nil) 158 return err == nil && format != nil 159 } 160 161 func toDependencies(s *sbom.SBOM, p pkg.Package) (out []string) { 162 for _, r := range s.Relationships { 163 if r.From.ID() == p.ID() { 164 if p, ok := r.To.(pkg.Package); ok { 165 out = append(out, dependencyName(p)) 166 } 167 } 168 } 169 return 170 } 171 172 // dependencyName to make things a little nicer to read; this might end up being lossy 173 func dependencyName(p pkg.Package) string { 174 purl, err := packageurl.FromString(p.PURL) 175 if err != nil { 176 log.Debugf("Invalid PURL for package: '%s' PURL: '%s' (%w)", p.Name, p.PURL, err) 177 return "" 178 } 179 // don't use qualifiers for this 180 purl.Qualifiers = nil 181 return purl.ToString() 182 } 183 184 func toDependencyScope(_ pkg.Package) DependencyScope { 185 return DependencyScopeRuntime 186 } 187 188 func toDependencyRelationshipType(_ pkg.Package) DependencyRelationship { 189 return DependencyRelationshipDirect 190 } 191 192 func toDependencyMetadata(_ pkg.Package) Metadata { 193 // We have limited properties: up to 8 with reasonably small values 194 // For now, we are encoding the location as part of the key, we are encoding PURLs with most 195 // of the other information Grype might need; and the distro information at the top level 196 // so we don't need anything here yet 197 return Metadata{} 198 }