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  }