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