github.com/anchore/syft@v1.38.2/syft/format/syftjson/to_syft_model.go (about)

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