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