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