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 }