github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/syftjson/to_syft_model.go (about)

     1  package syftjson
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  
    12  	stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
    13  	"github.com/anchore/syft/internal"
    14  	"github.com/anchore/syft/internal/log"
    15  	"github.com/anchore/syft/syft/artifact"
    16  	"github.com/anchore/syft/syft/cpe"
    17  	"github.com/anchore/syft/syft/file"
    18  	"github.com/anchore/syft/syft/formats/syftjson/model"
    19  	"github.com/anchore/syft/syft/linux"
    20  	"github.com/anchore/syft/syft/pkg"
    21  	"github.com/anchore/syft/syft/sbom"
    22  	"github.com/anchore/syft/syft/source"
    23  )
    24  
    25  func toSyftModel(doc model.Document) (*sbom.SBOM, error) {
    26  	idAliases := make(map[string]string)
    27  
    28  	catalog := toSyftCatalog(doc.Artifacts, idAliases)
    29  
    30  	fileArtifacts := toSyftFiles(doc.Files)
    31  
    32  	return &sbom.SBOM{
    33  		Artifacts: sbom.Artifacts{
    34  			Packages:          catalog,
    35  			FileMetadata:      fileArtifacts.FileMetadata,
    36  			FileDigests:       fileArtifacts.FileDigests,
    37  			FileContents:      fileArtifacts.FileContents,
    38  			FileLicenses:      fileArtifacts.FileLicenses,
    39  			LinuxDistribution: toSyftLinuxRelease(doc.Distro),
    40  		},
    41  		Source:        *toSyftSourceData(doc.Source),
    42  		Descriptor:    toSyftDescriptor(doc.Descriptor),
    43  		Relationships: warnConversionErrors(toSyftRelationships(&doc, catalog, doc.ArtifactRelationships, idAliases)),
    44  	}, nil
    45  }
    46  
    47  func warnConversionErrors[T any](converted []T, errors []error) []T {
    48  	errorMessages := deduplicateErrors(errors)
    49  	for _, msg := range errorMessages {
    50  		log.Warn(msg)
    51  	}
    52  	return converted
    53  }
    54  
    55  func deduplicateErrors(errors []error) []string {
    56  	errorCounts := make(map[string]int)
    57  	var errorMessages []string
    58  	for _, e := range errors {
    59  		errorCounts[e.Error()] = errorCounts[e.Error()] + 1
    60  	}
    61  	for msg, count := range errorCounts {
    62  		errorMessages = append(errorMessages, fmt.Sprintf("%q occurred %d time(s)", msg, count))
    63  	}
    64  	return errorMessages
    65  }
    66  
    67  func toSyftFiles(files []model.File) sbom.Artifacts {
    68  	ret := sbom.Artifacts{
    69  		FileMetadata: make(map[file.Coordinates]file.Metadata),
    70  		FileDigests:  make(map[file.Coordinates][]file.Digest),
    71  		FileContents: make(map[file.Coordinates]string),
    72  		FileLicenses: make(map[file.Coordinates][]file.License),
    73  	}
    74  
    75  	for _, f := range files {
    76  		coord := f.Location
    77  		if f.Metadata != nil {
    78  			mode, err := strconv.ParseInt(strconv.Itoa(f.Metadata.Mode), 8, 64)
    79  			if err != nil {
    80  				log.Warnf("invalid mode found in file catalog @ location=%+v mode=%q: %+v", coord, f.Metadata.Mode, err)
    81  				mode = 0
    82  			}
    83  
    84  			fm := os.FileMode(mode)
    85  
    86  			ret.FileMetadata[coord] = file.Metadata{
    87  				FileInfo: stereoscopeFile.ManualInfo{
    88  					NameValue: path.Base(coord.RealPath),
    89  					SizeValue: f.Metadata.Size,
    90  					ModeValue: fm,
    91  				},
    92  				Path:            coord.RealPath,
    93  				LinkDestination: f.Metadata.LinkDestination,
    94  				UserID:          f.Metadata.UserID,
    95  				GroupID:         f.Metadata.GroupID,
    96  				Type:            toSyftFileType(f.Metadata.Type),
    97  				MIMEType:        f.Metadata.MIMEType,
    98  			}
    99  		}
   100  
   101  		for _, d := range f.Digests {
   102  			ret.FileDigests[coord] = append(ret.FileDigests[coord], file.Digest{
   103  				Algorithm: d.Algorithm,
   104  				Value:     d.Value,
   105  			})
   106  		}
   107  
   108  		if f.Contents != "" {
   109  			ret.FileContents[coord] = f.Contents
   110  		}
   111  
   112  		for _, l := range f.Licenses {
   113  			var evidence *file.LicenseEvidence
   114  			if e := l.Evidence; e != nil {
   115  				evidence = &file.LicenseEvidence{
   116  					Confidence: e.Confidence,
   117  					Offset:     e.Offset,
   118  					Extent:     e.Extent,
   119  				}
   120  			}
   121  			ret.FileLicenses[coord] = append(ret.FileLicenses[coord], file.License{
   122  				Value:           l.Value,
   123  				SPDXExpression:  l.SPDXExpression,
   124  				Type:            l.Type,
   125  				LicenseEvidence: evidence,
   126  			})
   127  		}
   128  	}
   129  
   130  	return ret
   131  }
   132  
   133  func toSyftLicenses(m []model.License) (p []pkg.License) {
   134  	for _, l := range m {
   135  		p = append(p, pkg.License{
   136  			Value:          l.Value,
   137  			SPDXExpression: l.SPDXExpression,
   138  			Type:           l.Type,
   139  			URLs:           internal.NewStringSet(l.URLs...),
   140  			Locations:      file.NewLocationSet(l.Locations...),
   141  		})
   142  	}
   143  	return
   144  }
   145  
   146  func toSyftFileType(ty string) stereoscopeFile.Type {
   147  	switch ty {
   148  	case "SymbolicLink":
   149  		return stereoscopeFile.TypeSymLink
   150  	case "HardLink":
   151  		return stereoscopeFile.TypeHardLink
   152  	case "Directory":
   153  		return stereoscopeFile.TypeDirectory
   154  	case "Socket":
   155  		return stereoscopeFile.TypeSocket
   156  	case "BlockDevice":
   157  		return stereoscopeFile.TypeBlockDevice
   158  	case "CharacterDevice":
   159  		return stereoscopeFile.TypeCharacterDevice
   160  	case "FIFONode":
   161  		return stereoscopeFile.TypeFIFO
   162  	case "RegularFile":
   163  		return stereoscopeFile.TypeRegular
   164  	case "IrregularFile":
   165  		return stereoscopeFile.TypeIrregular
   166  	default:
   167  		return stereoscopeFile.TypeIrregular
   168  	}
   169  }
   170  
   171  func toSyftLinuxRelease(d model.LinuxRelease) *linux.Release {
   172  	if cmp.Equal(d, model.LinuxRelease{}) {
   173  		return nil
   174  	}
   175  	return &linux.Release{
   176  		PrettyName:       d.PrettyName,
   177  		Name:             d.Name,
   178  		ID:               d.ID,
   179  		IDLike:           d.IDLike,
   180  		Version:          d.Version,
   181  		VersionID:        d.VersionID,
   182  		VersionCodename:  d.VersionCodename,
   183  		BuildID:          d.BuildID,
   184  		ImageID:          d.ImageID,
   185  		ImageVersion:     d.ImageVersion,
   186  		Variant:          d.Variant,
   187  		VariantID:        d.VariantID,
   188  		HomeURL:          d.HomeURL,
   189  		SupportURL:       d.SupportURL,
   190  		BugReportURL:     d.BugReportURL,
   191  		PrivacyPolicyURL: d.PrivacyPolicyURL,
   192  		CPEName:          d.CPEName,
   193  		SupportEnd:       d.SupportEnd,
   194  	}
   195  }
   196  
   197  func toSyftRelationships(doc *model.Document, catalog *pkg.Collection, relationships []model.Relationship, idAliases map[string]string) ([]artifact.Relationship, []error) {
   198  	idMap := make(map[string]interface{})
   199  
   200  	for _, p := range catalog.Sorted() {
   201  		idMap[string(p.ID())] = p
   202  		locations := p.Locations.ToSlice()
   203  		for _, l := range locations {
   204  			idMap[string(l.Coordinates.ID())] = l.Coordinates
   205  		}
   206  	}
   207  
   208  	// set source metadata in identifier map
   209  	idMap[doc.Source.ID] = toSyftSource(doc.Source)
   210  
   211  	for _, f := range doc.Files {
   212  		idMap[f.ID] = f.Location
   213  	}
   214  
   215  	var out []artifact.Relationship
   216  	var conversionErrors []error
   217  	for _, r := range relationships {
   218  		syftRelationship, err := toSyftRelationship(idMap, r, idAliases)
   219  		if err != nil {
   220  			conversionErrors = append(conversionErrors, err)
   221  		}
   222  		if syftRelationship != nil {
   223  			out = append(out, *syftRelationship)
   224  		}
   225  	}
   226  
   227  	return out, conversionErrors
   228  }
   229  
   230  func toSyftSource(s model.Source) source.Source {
   231  	description := toSyftSourceData(s)
   232  	if description == nil {
   233  		return nil
   234  	}
   235  	return source.FromDescription(*description)
   236  }
   237  
   238  func toSyftRelationship(idMap map[string]interface{}, relationship model.Relationship, idAliases map[string]string) (*artifact.Relationship, error) {
   239  	id := func(id string) string {
   240  		aliased, ok := idAliases[id]
   241  		if ok {
   242  			return aliased
   243  		}
   244  		return id
   245  	}
   246  
   247  	from, ok := idMap[id(relationship.Parent)].(artifact.Identifiable)
   248  	if !ok {
   249  		return nil, fmt.Errorf("relationship mapping from key %s is not a valid artifact.Identifiable type: %+v", relationship.Parent, idMap[relationship.Parent])
   250  	}
   251  
   252  	to, ok := idMap[id(relationship.Child)].(artifact.Identifiable)
   253  	if !ok {
   254  		return nil, fmt.Errorf("relationship mapping to key %s is not a valid artifact.Identifiable type: %+v", relationship.Child, idMap[relationship.Child])
   255  	}
   256  
   257  	typ := artifact.RelationshipType(relationship.Type)
   258  
   259  	switch typ {
   260  	case artifact.OwnershipByFileOverlapRelationship, artifact.ContainsRelationship, artifact.DependencyOfRelationship, artifact.EvidentByRelationship:
   261  	default:
   262  		if !strings.Contains(string(typ), "dependency-of") {
   263  			return nil, fmt.Errorf("unknown relationship type: %s", string(typ))
   264  		}
   265  		// lets try to stay as compatible as possible with similar relationship types without dropping the relationship
   266  		log.Warnf("assuming %q for relationship type %q", artifact.DependencyOfRelationship, typ)
   267  		typ = artifact.DependencyOfRelationship
   268  	}
   269  	return &artifact.Relationship{
   270  		From: from,
   271  		To:   to,
   272  		Type: typ,
   273  		Data: relationship.Metadata,
   274  	}, nil
   275  }
   276  
   277  func toSyftDescriptor(d model.Descriptor) sbom.Descriptor {
   278  	return sbom.Descriptor{
   279  		Name:          d.Name,
   280  		Version:       d.Version,
   281  		Configuration: d.Configuration,
   282  	}
   283  }
   284  
   285  func toSyftSourceData(s model.Source) *source.Description {
   286  	return &source.Description{
   287  		ID:       s.ID,
   288  		Name:     s.Name,
   289  		Version:  s.Version,
   290  		Metadata: s.Metadata,
   291  	}
   292  }
   293  
   294  func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Collection {
   295  	catalog := pkg.NewCollection()
   296  	for _, p := range pkgs {
   297  		catalog.Add(toSyftPackage(p, idAliases))
   298  	}
   299  	return catalog
   300  }
   301  
   302  func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
   303  	var cpes []cpe.CPE
   304  	for _, c := range p.CPEs {
   305  		value, err := cpe.New(c)
   306  		if err != nil {
   307  			log.Warnf("excluding invalid CPE %q: %v", c, err)
   308  			continue
   309  		}
   310  
   311  		cpes = append(cpes, value)
   312  	}
   313  
   314  	out := pkg.Package{
   315  		Name:         p.Name,
   316  		Version:      p.Version,
   317  		FoundBy:      p.FoundBy,
   318  		Locations:    file.NewLocationSet(p.Locations...),
   319  		Licenses:     pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
   320  		Language:     p.Language,
   321  		Type:         p.Type,
   322  		CPEs:         cpes,
   323  		PURL:         p.PURL,
   324  		MetadataType: p.MetadataType,
   325  		Metadata:     p.Metadata,
   326  	}
   327  
   328  	// we don't know if this package ID is truly unique, however, we need to trust the user input in case there are
   329  	// external references to it. That is, we can't derive our own ID (using pkg.SetID()) since consumers won't
   330  	// be able to historically interact with data that references the IDs from the original SBOM document being decoded now.
   331  	out.OverrideID(artifact.ID(p.ID))
   332  
   333  	// this alias mapping is currently defunct, but could be useful in the future.
   334  	id := string(out.ID())
   335  	if id != p.ID {
   336  		idAliases[p.ID] = id
   337  	}
   338  
   339  	return out
   340  }