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  }