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  }