github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/formats/gosbomjson/to_format_model.go (about)

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