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