github.com/anchore/syft@v1.38.2/syft/format/syftjson/to_format_model.go (about)

     1  package syftjson
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strconv"
     7  
     8  	stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
     9  	"github.com/anchore/syft/internal"
    10  	"github.com/anchore/syft/internal/log"
    11  	"github.com/anchore/syft/internal/packagemetadata"
    12  	"github.com/anchore/syft/internal/sourcemetadata"
    13  	"github.com/anchore/syft/syft/artifact"
    14  	"github.com/anchore/syft/syft/file"
    15  	formatInternal "github.com/anchore/syft/syft/format/internal"
    16  	"github.com/anchore/syft/syft/format/syftjson/model"
    17  	"github.com/anchore/syft/syft/linux"
    18  	"github.com/anchore/syft/syft/pkg"
    19  	"github.com/anchore/syft/syft/sbom"
    20  	"github.com/anchore/syft/syft/source"
    21  )
    22  
    23  // MetadataType infers the metadata type value based on the pkg.Metadata payload.
    24  func MetadataType(metadata interface{}) string {
    25  	return metadataType(metadata, false)
    26  }
    27  
    28  func metadataType(metadata interface{}, legacy bool) string {
    29  	if legacy {
    30  		return packagemetadata.JSONLegacyName(metadata)
    31  	}
    32  	return packagemetadata.JSONName(metadata)
    33  }
    34  
    35  // ToFormatModel transforms the sbom import a format-specific model.
    36  func ToFormatModel(s sbom.SBOM, cfg EncoderConfig) model.Document {
    37  	locationSorter, coordinateSorter := formatInternal.GetLocationSorters(s)
    38  
    39  	return model.Document{
    40  		Artifacts:             toPackageModels(s.Artifacts.Packages, locationSorter, cfg),
    41  		ArtifactRelationships: toRelationshipModel(s.Relationships),
    42  		Files:                 toFile(s, coordinateSorter),
    43  		Source:                toSourceModel(s.Source),
    44  		Distro:                toLinuxRelease(s.Artifacts.LinuxDistribution),
    45  		Descriptor:            toDescriptor(s.Descriptor),
    46  		Schema: model.Schema{
    47  			Version: internal.JSONSchemaVersion,
    48  			URL:     fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
    49  		},
    50  	}
    51  }
    52  
    53  func toLinuxRelease(d *linux.Release) model.LinuxRelease {
    54  	if d == nil {
    55  		return model.LinuxRelease{}
    56  	}
    57  	return model.LinuxRelease{
    58  		PrettyName:       d.PrettyName,
    59  		Name:             d.Name,
    60  		ID:               d.ID,
    61  		IDLike:           d.IDLike,
    62  		Version:          d.Version,
    63  		VersionID:        d.VersionID,
    64  		VersionCodename:  d.VersionCodename,
    65  		BuildID:          d.BuildID,
    66  		ImageID:          d.ImageID,
    67  		ImageVersion:     d.ImageVersion,
    68  		Variant:          d.Variant,
    69  		VariantID:        d.VariantID,
    70  		HomeURL:          d.HomeURL,
    71  		SupportURL:       d.SupportURL,
    72  		BugReportURL:     d.BugReportURL,
    73  		PrivacyPolicyURL: d.PrivacyPolicyURL,
    74  		CPEName:          d.CPEName,
    75  		SupportEnd:       d.SupportEnd,
    76  		ExtendedSupport:  d.ExtendedSupport,
    77  	}
    78  }
    79  
    80  func toDescriptor(d sbom.Descriptor) model.Descriptor {
    81  	return model.Descriptor{
    82  		Name:          d.Name,
    83  		Version:       d.Version,
    84  		Configuration: d.Configuration,
    85  	}
    86  }
    87  
    88  func toFile(s sbom.SBOM, coordinateSorter func(a, b file.Coordinates) int) []model.File {
    89  	results := make([]model.File, 0)
    90  	artifacts := s.Artifacts
    91  
    92  	for _, coordinates := range s.AllCoordinates() {
    93  		var metadata *file.Metadata
    94  		if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
    95  			metadata = &metadataForLocation
    96  		}
    97  
    98  		var digests []file.Digest
    99  		if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
   100  			digests = digestsForLocation
   101  		}
   102  
   103  		var contents string
   104  		if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists {
   105  			contents = contentsForLocation
   106  		}
   107  
   108  		var unknowns []string
   109  		if unknownsForLocation, exists := artifacts.Unknowns[coordinates]; exists {
   110  			unknowns = unknownsForLocation
   111  		}
   112  
   113  		var licenses []model.FileLicense
   114  		for _, l := range artifacts.FileLicenses[coordinates] {
   115  			var evidence *model.FileLicenseEvidence
   116  			if e := l.LicenseEvidence; e != nil {
   117  				evidence = &model.FileLicenseEvidence{
   118  					Confidence: e.Confidence,
   119  					Offset:     e.Offset,
   120  					Extent:     e.Extent,
   121  				}
   122  			}
   123  			licenses = append(licenses, model.FileLicense{
   124  				Value:          l.Value,
   125  				SPDXExpression: l.SPDXExpression,
   126  				Type:           l.Type,
   127  				Evidence:       evidence,
   128  			})
   129  		}
   130  
   131  		var executable *file.Executable
   132  		if exec, exists := artifacts.Executables[coordinates]; exists {
   133  			executable = &exec
   134  		}
   135  
   136  		results = append(results, model.File{
   137  			ID:         string(coordinates.ID()),
   138  			Location:   coordinates,
   139  			Metadata:   toFileMetadataEntry(coordinates, metadata),
   140  			Digests:    digests,
   141  			Contents:   contents,
   142  			Licenses:   licenses,
   143  			Executable: executable,
   144  			Unknowns:   unknowns,
   145  		})
   146  	}
   147  
   148  	// sort to ensure we're stable across multiple runs
   149  	// should order by the layer order from the container image then by real path
   150  	sortFiles(results, coordinateSorter)
   151  	return results
   152  }
   153  
   154  func sortFiles(files []model.File, coordinateSorter func(a, b file.Coordinates) int) {
   155  	fileSorter := func(a, b model.File) int {
   156  		return coordinateSorter(a.Location, b.Location)
   157  	}
   158  	sort.SliceStable(files, func(i, j int) bool {
   159  		return fileSorter(files[i], files[j]) < 0
   160  	})
   161  }
   162  
   163  func toFileMetadataEntry(coordinates file.Coordinates, metadata *file.Metadata) *model.FileMetadataEntry {
   164  	if metadata == nil {
   165  		return nil
   166  	}
   167  
   168  	var mode int
   169  	var size int64
   170  	if metadata.FileInfo != nil {
   171  		var err error
   172  
   173  		mode, err = strconv.Atoi(fmt.Sprintf("%o", metadata.Mode()))
   174  		if err != nil {
   175  			log.Debugf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err)
   176  			mode = 0
   177  		}
   178  
   179  		size = metadata.Size()
   180  	}
   181  
   182  	return &model.FileMetadataEntry{
   183  		Mode:            mode,
   184  		Type:            toFileType(metadata.Type),
   185  		LinkDestination: metadata.LinkDestination,
   186  		UserID:          metadata.UserID,
   187  		GroupID:         metadata.GroupID,
   188  		MIMEType:        metadata.MIMEType,
   189  		Size:            size,
   190  	}
   191  }
   192  
   193  func toFileType(ty stereoscopeFile.Type) string {
   194  	switch ty {
   195  	case stereoscopeFile.TypeSymLink:
   196  		return "SymbolicLink"
   197  	case stereoscopeFile.TypeHardLink:
   198  		return "HardLink"
   199  	case stereoscopeFile.TypeDirectory:
   200  		return "Directory"
   201  	case stereoscopeFile.TypeSocket:
   202  		return "Socket"
   203  	case stereoscopeFile.TypeBlockDevice:
   204  		return "BlockDevice"
   205  	case stereoscopeFile.TypeCharacterDevice:
   206  		return "CharacterDevice"
   207  	case stereoscopeFile.TypeFIFO:
   208  		return "FIFONode"
   209  	case stereoscopeFile.TypeRegular:
   210  		return "RegularFile"
   211  	case stereoscopeFile.TypeIrregular:
   212  		return "IrregularFile"
   213  	default:
   214  		return "Unknown"
   215  	}
   216  }
   217  
   218  func toPackageModels(catalog *pkg.Collection, locationSorter func(a, b file.Location) int, cfg EncoderConfig) []model.Package {
   219  	artifacts := make([]model.Package, 0)
   220  	if catalog == nil {
   221  		return artifacts
   222  	}
   223  	for _, p := range catalog.Sorted() {
   224  		artifacts = append(artifacts, toPackageModel(p, locationSorter, cfg))
   225  	}
   226  	return artifacts
   227  }
   228  
   229  func toLicenseModel(pkgLicenses []pkg.License, locationSorter func(a, b file.Location) int) (modelLicenses []model.License) {
   230  	for _, l := range pkgLicenses {
   231  		// format model must have allocated collections
   232  		urls := l.URLs
   233  		if urls == nil {
   234  			urls = []string{}
   235  		}
   236  
   237  		modelLicenses = append(modelLicenses, model.License{
   238  			Value:          l.Value,
   239  			SPDXExpression: l.SPDXExpression,
   240  			Contents:       l.Contents,
   241  			Type:           l.Type,
   242  			URLs:           urls,
   243  			Locations:      toLocationsModel(l.Locations, locationSorter),
   244  		})
   245  	}
   246  	return
   247  }
   248  
   249  // toPackageModel crates a new Package from the given pkg.Package.
   250  func toPackageModel(p pkg.Package, locationSorter func(a, b file.Location) int, cfg EncoderConfig) model.Package {
   251  	var cpes = make([]model.CPE, len(p.CPEs))
   252  	for i, c := range p.CPEs {
   253  		convertedCPE := model.CPE{
   254  			Value:  c.Attributes.String(),
   255  			Source: c.Source.String(),
   256  		}
   257  		cpes[i] = convertedCPE
   258  	}
   259  
   260  	// we want to make sure all catalogers are
   261  	// initializing the array; this is a good choke point for this check
   262  	var licenses = make([]model.License, 0)
   263  	if !p.Licenses.Empty() {
   264  		licenses = toLicenseModel(p.Licenses.ToSlice(), locationSorter)
   265  	}
   266  
   267  	return model.Package{
   268  		PackageBasicData: model.PackageBasicData{
   269  			ID:        string(p.ID()),
   270  			Name:      p.Name,
   271  			Version:   p.Version,
   272  			Type:      p.Type,
   273  			FoundBy:   p.FoundBy,
   274  			Locations: toLocationsModel(p.Locations, locationSorter),
   275  			Licenses:  licenses,
   276  			Language:  p.Language,
   277  			CPEs:      cpes,
   278  			PURL:      p.PURL,
   279  		},
   280  		PackageCustomData: model.PackageCustomData{
   281  			MetadataType: metadataType(p.Metadata, cfg.Legacy),
   282  			Metadata:     p.Metadata,
   283  		},
   284  	}
   285  }
   286  
   287  func toLocationsModel(locations file.LocationSet, locationSorter func(a, b file.Location) int) []file.Location {
   288  	if locations.Empty() {
   289  		return []file.Location{}
   290  	}
   291  
   292  	return locations.ToSlice(locationSorter)
   293  }
   294  
   295  func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship {
   296  	result := make([]model.Relationship, len(relationships))
   297  	for i, r := range relationships {
   298  		result[i] = model.Relationship{
   299  			Parent:   string(r.From.ID()),
   300  			Child:    string(r.To.ID()),
   301  			Type:     string(r.Type),
   302  			Metadata: r.Data,
   303  		}
   304  	}
   305  	sort.Slice(result, func(i, j int) bool {
   306  		if iParent, jParent := result[i].Parent, result[j].Parent; iParent != jParent {
   307  			return iParent < jParent
   308  		}
   309  		if iChild, jChild := result[i].Child, result[j].Child; iChild != jChild {
   310  			return iChild < jChild
   311  		}
   312  		return result[i].Type < result[j].Type
   313  	})
   314  	return result
   315  }
   316  
   317  // toSourceModel creates a new source object to be represented into JSON.
   318  func toSourceModel(src source.Description) model.Source {
   319  	m := model.Source{
   320  		ID:       src.ID,
   321  		Name:     src.Name,
   322  		Version:  src.Version,
   323  		Supplier: src.Supplier,
   324  		Type:     sourcemetadata.JSONName(src.Metadata),
   325  		Metadata: src.Metadata,
   326  	}
   327  
   328  	if metadata, ok := src.Metadata.(source.ImageMetadata); ok {
   329  		// ensure that empty collections are not shown as null
   330  		if metadata.RepoDigests == nil {
   331  			metadata.RepoDigests = []string{}
   332  		}
   333  		if metadata.Tags == nil {
   334  			metadata.Tags = []string{}
   335  		}
   336  		m.Metadata = metadata
   337  	}
   338  
   339  	return m
   340  }