github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/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/kastenhq/syft/internal"
    10  	"github.com/kastenhq/syft/internal/log"
    11  	"github.com/kastenhq/syft/syft/artifact"
    12  	"github.com/kastenhq/syft/syft/cpe"
    13  	"github.com/kastenhq/syft/syft/file"
    14  	"github.com/kastenhq/syft/syft/formats/syftjson/model"
    15  	"github.com/kastenhq/syft/syft/internal/sourcemetadata"
    16  	"github.com/kastenhq/syft/syft/linux"
    17  	"github.com/kastenhq/syft/syft/pkg"
    18  	"github.com/kastenhq/syft/syft/sbom"
    19  	"github.com/kastenhq/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  		results = append(results, model.File{
   110  			ID:       string(coordinates.ID()),
   111  			Location: coordinates,
   112  			Metadata: toFileMetadataEntry(coordinates, metadata),
   113  			Digests:  digests,
   114  			Contents: contents,
   115  		})
   116  	}
   117  
   118  	// sort by real path then virtual path to ensure the result is stable across multiple runs
   119  	sort.SliceStable(results, func(i, j int) bool {
   120  		return results[i].Location.RealPath < results[j].Location.RealPath
   121  	})
   122  	return results
   123  }
   124  
   125  func toFileMetadataEntry(coordinates file.Coordinates, metadata *file.Metadata) *model.FileMetadataEntry {
   126  	if metadata == nil {
   127  		return nil
   128  	}
   129  
   130  	var mode int
   131  	var size int64
   132  	if metadata != nil && metadata.FileInfo != nil {
   133  		var err error
   134  
   135  		mode, err = strconv.Atoi(fmt.Sprintf("%o", metadata.Mode()))
   136  		if err != nil {
   137  			log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coordinates, metadata.Mode, err)
   138  			mode = 0
   139  		}
   140  
   141  		size = metadata.Size()
   142  	}
   143  
   144  	return &model.FileMetadataEntry{
   145  		Mode:            mode,
   146  		Type:            toFileType(metadata.Type),
   147  		LinkDestination: metadata.LinkDestination,
   148  		UserID:          metadata.UserID,
   149  		GroupID:         metadata.GroupID,
   150  		MIMEType:        metadata.MIMEType,
   151  		Size:            size,
   152  	}
   153  }
   154  
   155  func toFileType(ty stereoscopeFile.Type) string {
   156  	switch ty {
   157  	case stereoscopeFile.TypeSymLink:
   158  		return "SymbolicLink"
   159  	case stereoscopeFile.TypeHardLink:
   160  		return "HardLink"
   161  	case stereoscopeFile.TypeDirectory:
   162  		return "Directory"
   163  	case stereoscopeFile.TypeSocket:
   164  		return "Socket"
   165  	case stereoscopeFile.TypeBlockDevice:
   166  		return "BlockDevice"
   167  	case stereoscopeFile.TypeCharacterDevice:
   168  		return "CharacterDevice"
   169  	case stereoscopeFile.TypeFIFO:
   170  		return "FIFONode"
   171  	case stereoscopeFile.TypeRegular:
   172  		return "RegularFile"
   173  	case stereoscopeFile.TypeIrregular:
   174  		return "IrregularFile"
   175  	default:
   176  		return "Unknown"
   177  	}
   178  }
   179  
   180  func toPackageModels(catalog *pkg.Collection) []model.Package {
   181  	artifacts := make([]model.Package, 0)
   182  	if catalog == nil {
   183  		return artifacts
   184  	}
   185  	for _, p := range catalog.Sorted() {
   186  		artifacts = append(artifacts, toPackageModel(p))
   187  	}
   188  	return artifacts
   189  }
   190  
   191  func toLicenseModel(pkgLicenses []pkg.License) (modelLicenses []model.License) {
   192  	for _, l := range pkgLicenses {
   193  		// guarantee collection
   194  		locations := make([]file.Location, 0)
   195  		if v := l.Locations.ToSlice(); v != nil {
   196  			locations = v
   197  		}
   198  		modelLicenses = append(modelLicenses, model.License{
   199  			Value:          l.Value,
   200  			SPDXExpression: l.SPDXExpression,
   201  			Type:           l.Type,
   202  			URLs:           l.URLs.ToSlice(),
   203  			Locations:      locations,
   204  		})
   205  	}
   206  	return
   207  }
   208  
   209  // toPackageModel crates a new Package from the given pkg.Package.
   210  func toPackageModel(p pkg.Package) model.Package {
   211  	var cpes = make([]string, len(p.CPEs))
   212  	for i, c := range p.CPEs {
   213  		cpes[i] = cpe.String(c)
   214  	}
   215  
   216  	// we want to make sure all catalogers are
   217  	// initializing the array; this is a good choke point for this check
   218  	var licenses = make([]model.License, 0)
   219  	if !p.Licenses.Empty() {
   220  		licenses = toLicenseModel(p.Licenses.ToSlice())
   221  	}
   222  
   223  	return model.Package{
   224  		PackageBasicData: model.PackageBasicData{
   225  			ID:        string(p.ID()),
   226  			Name:      p.Name,
   227  			Version:   p.Version,
   228  			Type:      p.Type,
   229  			FoundBy:   p.FoundBy,
   230  			Locations: p.Locations.ToSlice(),
   231  			Licenses:  licenses,
   232  			Language:  p.Language,
   233  			CPEs:      cpes,
   234  			PURL:      p.PURL,
   235  		},
   236  		PackageCustomData: model.PackageCustomData{
   237  			MetadataType: p.MetadataType,
   238  			Metadata:     p.Metadata,
   239  		},
   240  	}
   241  }
   242  
   243  func toRelationshipModel(relationships []artifact.Relationship) []model.Relationship {
   244  	result := make([]model.Relationship, len(relationships))
   245  	for i, r := range relationships {
   246  		result[i] = model.Relationship{
   247  			Parent:   string(r.From.ID()),
   248  			Child:    string(r.To.ID()),
   249  			Type:     string(r.Type),
   250  			Metadata: r.Data,
   251  		}
   252  	}
   253  	sort.Slice(result, func(i, j int) bool {
   254  		if iParent, jParent := result[i].Parent, result[j].Parent; iParent != jParent {
   255  			return iParent < jParent
   256  		}
   257  		if iChild, jChild := result[i].Child, result[j].Child; iChild != jChild {
   258  			return iChild < jChild
   259  		}
   260  		return result[i].Type < result[j].Type
   261  	})
   262  	return result
   263  }
   264  
   265  // toSourceModel creates a new source object to be represented into JSON.
   266  func toSourceModel(src source.Description) model.Source {
   267  	m := model.Source{
   268  		ID:       src.ID,
   269  		Name:     src.Name,
   270  		Version:  src.Version,
   271  		Type:     sourcemetadata.JSONName(src.Metadata),
   272  		Metadata: src.Metadata,
   273  	}
   274  
   275  	if metadata, ok := src.Metadata.(source.StereoscopeImageSourceMetadata); ok {
   276  		// ensure that empty collections are not shown as null
   277  		if metadata.RepoDigests == nil {
   278  			metadata.RepoDigests = []string{}
   279  		}
   280  		if metadata.Tags == nil {
   281  			metadata.Tags = []string{}
   282  		}
   283  		m.Metadata = metadata
   284  	}
   285  
   286  	return m
   287  }