github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/binary/pe_package.go (about)

     1  package binary
     2  
     3  import (
     4  	"path"
     5  	"regexp"
     6  	"sort"
     7  	"strings"
     8  
     9  	packageurl "github.com/anchore/packageurl-go"
    10  	"github.com/anchore/syft/syft/file"
    11  	"github.com/anchore/syft/syft/pkg"
    12  )
    13  
    14  var (
    15  	// spaceRegex includes nbsp (#160) considered to be a space character
    16  	spaceRegex  = regexp.MustCompile(`[\s\xa0]+`)
    17  	numberRegex = regexp.MustCompile(`\d`)
    18  )
    19  
    20  func newPEPackage(versionResources map[string]string, f file.Location) pkg.Package {
    21  	name := findNameFromVR(versionResources)
    22  
    23  	if name == "" {
    24  		// it's possible that the version resources are empty, so we fall back to the file name
    25  		name = strings.TrimSuffix(strings.TrimSuffix(path.Base(f.RealPath), ".exe"), ".dll")
    26  	}
    27  
    28  	p := pkg.Package{
    29  		Name:      name,
    30  		Version:   findVersionFromVR(versionResources),
    31  		Locations: file.NewLocationSet(f.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
    32  		Type:      pkg.BinaryPkg,
    33  		Metadata:  newPEBinaryVersionResourcesFromMap(versionResources),
    34  	}
    35  
    36  	// If this appears to be Ghostscript, emit a canonical generic purl
    37  	// Example expected: pkg:generic/ghostscript@<version>
    38  	prod := strings.ToLower(spaceNormalize(versionResources["ProductName"]))
    39  	if prod == "" {
    40  		// fall back to FileDescription if ProductName is missing
    41  		prod = strings.ToLower(spaceNormalize(versionResources["FileDescription"]))
    42  	}
    43  	if p.Version != "" && strings.Contains(prod, "ghostscript") {
    44  		// build a generic PURL for ghostscript
    45  		purl := packageurl.NewPackageURL(packageurl.TypeGeneric, "", "ghostscript", p.Version, nil, "").ToString()
    46  		p.PURL = purl
    47  	}
    48  
    49  	p.SetID()
    50  
    51  	return p
    52  }
    53  
    54  func newPEBinaryVersionResourcesFromMap(vr map[string]string) pkg.PEBinary {
    55  	var kvs pkg.KeyValues
    56  	for k, v := range vr {
    57  		if v == "" {
    58  			continue
    59  		}
    60  		kvs = append(kvs, pkg.KeyValue{
    61  			Key:   k,
    62  			Value: spaceNormalize(v),
    63  		})
    64  	}
    65  
    66  	sort.Slice(kvs, func(i, j int) bool {
    67  		return kvs[i].Key < kvs[j].Key
    68  	})
    69  
    70  	return pkg.PEBinary{
    71  		VersionResources: kvs,
    72  	}
    73  }
    74  
    75  func findNameFromVR(versionResources map[string]string) string {
    76  	// PE files not authored by Microsoft tend to use ProductName as an identifier.
    77  	nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"}
    78  
    79  	if isMicrosoftVR(versionResources) {
    80  		// for Microsoft files, prioritize FileDescription.
    81  		nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"}
    82  	}
    83  
    84  	var name string
    85  	for _, field := range nameFields {
    86  		value := spaceNormalize(versionResources[field])
    87  		if value == "" {
    88  			continue
    89  		}
    90  		name = value
    91  		break
    92  	}
    93  
    94  	return name
    95  }
    96  func isMicrosoftVR(versionResources map[string]string) bool {
    97  	return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") ||
    98  		strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft")
    99  }
   100  
   101  // spaceNormalize trims and normalizes whitespace in a string.
   102  func spaceNormalize(value string) string {
   103  	value = strings.TrimSpace(value)
   104  	if value == "" {
   105  		return ""
   106  	}
   107  	// ensure valid UTF-8.
   108  	value = strings.ToValidUTF8(value, "")
   109  	// consolidate all whitespace.
   110  	value = spaceRegex.ReplaceAllString(value, " ")
   111  	// remove non-printable characters.
   112  	value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "")
   113  	// consolidate again and trim.
   114  	value = spaceRegex.ReplaceAllString(value, " ")
   115  	value = strings.TrimSpace(value)
   116  	return value
   117  }
   118  
   119  func findVersionFromVR(versionResources map[string]string) string {
   120  	productVersion := extractVersionFromResourcesValue(versionResources["ProductVersion"])
   121  	fileVersion := extractVersionFromResourcesValue(versionResources["FileVersion"])
   122  
   123  	if productVersion != "" {
   124  		return productVersion
   125  	}
   126  
   127  	return fileVersion
   128  }
   129  
   130  func extractVersionFromResourcesValue(version string) string {
   131  	version = strings.TrimSpace(version)
   132  	out := ""
   133  	for i, f := range strings.Fields(version) {
   134  		if containsNumber(out) && !containsNumber(f) {
   135  			return out
   136  		}
   137  		if i == 0 {
   138  			out = f
   139  		} else {
   140  			out += " " + f
   141  		}
   142  	}
   143  	return out
   144  }
   145  
   146  func containsNumber(s string) bool {
   147  	return numberRegex.MatchString(s)
   148  }