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