github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/dotnet/package.go (about) 1 package dotnet 2 3 import ( 4 "fmt" 5 "path" 6 "regexp" 7 "strconv" 8 "strings" 9 10 "github.com/anchore/go-version" 11 "github.com/anchore/packageurl-go" 12 "github.com/anchore/syft/internal/log" 13 "github.com/anchore/syft/syft/cpe" 14 "github.com/anchore/syft/syft/file" 15 "github.com/anchore/syft/syft/pkg" 16 ) 17 18 var ( 19 // spaceRegex includes nbsp (#160) considered to be a space character 20 spaceRegex = regexp.MustCompile(`[\s\xa0]+`) 21 numberRegex = regexp.MustCompile(`\d`) 22 versionPunctuationRegex = regexp.MustCompile(`[.,]+`) 23 ) 24 25 // newDotnetDepsPackage creates a new Dotnet dependency package from a logicalDepsJSONPackage. 26 // Note that the new logicalDepsJSONPackage now directly holds library and executable information. 27 func newDotnetDepsPackage(lp logicalDepsJSONPackage, depsLocation file.Location) *pkg.Package { 28 name, ver := extractNameAndVersion(lp.NameVersion) 29 locs := file.NewLocationSet(depsLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) 30 31 for _, pe := range lp.Executables { 32 locs.Add(pe.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation)) 33 } 34 35 m := newDotnetDepsEntry(lp) 36 37 var cpes []cpe.CPE 38 if isRuntime(name) { 39 cpes = runtimeCPEs(ver) 40 } 41 42 p := &pkg.Package{ 43 Name: name, 44 Version: ver, 45 Locations: locs, 46 PURL: packageURL(m), 47 Language: pkg.Dotnet, 48 Type: pkg.DotnetPkg, 49 CPEs: cpes, 50 Metadata: m, 51 } 52 53 p.SetID() 54 55 return p 56 } 57 58 func isRuntime(name string) bool { 59 // found in a self-contained net8 app in the deps.json for the application 60 selfContainedRuntimeDependency := strings.HasPrefix(name, "runtimepack.Microsoft.NETCore.App.Runtime") 61 // found in net8 apps in the deps.json for the runtime 62 explicitRuntimeDependency := strings.HasPrefix(name, "Microsoft.NETCore.App.Runtime") 63 // found in net2 apps in the deps.json for the runtime 64 producesARuntime := strings.HasPrefix(name, "runtime") && strings.HasSuffix(name, "Microsoft.NETCore.App") 65 return selfContainedRuntimeDependency || explicitRuntimeDependency || producesARuntime 66 } 67 68 func runtimeCPEs(ver string) []cpe.CPE { 69 // .NET Core Versions 70 // 2016: .NET Core 1.0, cpe:2.3:a:microsoft:dotnet_core:1.0:*:*:*:*:*:*:* 71 // 2016: .NET Core 1.1, cpe:2.3:a:microsoft:dotnet_core:1.1:*:*:*:*:*:*:* 72 // 2017: .NET Core 2.0, cpe:2.3:a:microsoft:dotnet_core:2.0:*:*:*:*:*:*:* 73 // 2018: .NET Core 2.1, cpe:2.3:a:microsoft:dotnet_core:2.1:*:*:*:*:*:*:* 74 // 2018: .NET Core 2.2, cpe:2.3:a:microsoft:dotnet_core:2.2:*:*:*:*:*:*:* 75 // 2019: .NET Core 3.0, cpe:2.3:a:microsoft:dotnet_core:3.0:*:*:*:*:*:*:* 76 // 2019: .NET Core 3.1, cpe:2.3:a:microsoft:dotnet_core:3.1:*:*:*:*:*:*:* 77 78 // Unified .NET Versions 79 // 2020: .NET 5.0, cpe:2.3:a:microsoft:dotnet:5.0:*:*:*:*:*:*:* 80 // 2021: .NET 6.0, cpe:2.3:a:microsoft:dotnet:6.0:*:*:*:*:*:*:* 81 // 2022: .NET 7.0, cpe:2.3:a:microsoft:dotnet:7.0:*:*:*:*:*:*:* 82 // 2023: .NET 8.0, cpe:2.3:a:microsoft:dotnet:8.0:*:*:*:*:*:*:* 83 // 2024: .NET 9.0, cpe:2.3:a:microsoft:dotnet:9.0:*:*:*:*:*:*:* 84 // 2025 ...? 85 86 fields := strings.Split(ver, ".") 87 majorVersion, err := strconv.Atoi(fields[0]) 88 if err != nil { 89 log.WithFields("error", err).Tracef("failed to parse .NET major version from %q", ver) 90 return nil 91 } 92 93 var minorVersion int 94 if len(fields) > 1 { 95 minorVersion, err = strconv.Atoi(fields[1]) 96 if err != nil { 97 log.WithFields("error", err).Tracef("failed to parse .NET minor version from %q", ver) 98 return nil 99 } 100 } 101 102 productName := "dotnet" 103 if majorVersion < 5 { 104 productName = "dotnet_core" 105 } 106 107 return []cpe.CPE{ 108 { 109 Attributes: cpe.Attributes{ 110 Part: "a", 111 Vendor: "microsoft", 112 Product: productName, 113 Version: fmt.Sprintf("%d.%d", majorVersion, minorVersion), 114 }, 115 // we didn't find this in the underlying material, but this is the convention in NVD and we are certain this is a runtime package 116 Source: cpe.DeclaredSource, 117 }, 118 } 119 } 120 121 // newDotnetDepsEntry creates a Dotnet dependency entry using the new logicalDepsJSONPackage. 122 func newDotnetDepsEntry(lp logicalDepsJSONPackage) pkg.DotnetDepsEntry { 123 name, ver := extractNameAndVersion(lp.NameVersion) 124 125 // since this is a metadata type, we should not allocate this collection unless there are entries; otherwise 126 // the JSON serialization will produce an empty object instead of omitting the field. 127 var pes map[string]pkg.DotnetPortableExecutableEntry 128 if len(lp.Executables) > 0 { 129 pes = make(map[string]pkg.DotnetPortableExecutableEntry) 130 for _, pe := range lp.Executables { 131 pes[pe.TargetPath] = newDotnetPortableExecutableEntry(pe) 132 } 133 } 134 135 var path, sha, hashPath string 136 lib := lp.Library 137 if lib != nil { 138 path = lib.Path 139 sha = lib.Sha512 140 hashPath = lib.HashPath 141 } 142 143 return pkg.DotnetDepsEntry{ 144 Name: name, 145 Version: ver, 146 Path: path, 147 Sha512: sha, 148 HashPath: hashPath, 149 Executables: pes, 150 } 151 } 152 153 // newDotnetPortableExecutableEntry creates a portable executable entry from a File. 154 func newDotnetPortableExecutableEntry(pe logicalPE) pkg.DotnetPortableExecutableEntry { 155 return newDotnetPortableExecutableEntryFromMap(pe.VersionResources) 156 } 157 158 func newDotnetPortableExecutableEntryFromMap(vr map[string]string) pkg.DotnetPortableExecutableEntry { 159 return pkg.DotnetPortableExecutableEntry{ 160 // for some reason, the assembly version is sometimes stored as "Assembly Version" and sometimes as "AssemblyVersion" 161 AssemblyVersion: cleanVersionResourceField(vr["Assembly Version"], vr["AssemblyVersion"]), 162 LegalCopyright: cleanVersionResourceField(vr["LegalCopyright"]), 163 Comments: cleanVersionResourceField(vr["Comments"]), 164 InternalName: cleanVersionResourceField(vr["InternalName"]), 165 CompanyName: cleanVersionResourceField(vr["CompanyName"]), 166 ProductName: cleanVersionResourceField(vr["ProductName"]), 167 ProductVersion: cleanVersionResourceField(vr["ProductVersion"]), 168 } 169 } 170 171 func cleanVersionResourceField(values ...string) string { 172 for _, value := range values { 173 if value == "" { 174 continue 175 } 176 return strings.TrimSpace(value) 177 } 178 return "" 179 } 180 181 func getDepsJSONFilePrefix(p string) string { 182 r := regexp.MustCompile(`([^\\\/]+)\.deps\.json$`) 183 match := r.FindStringSubmatch(p) 184 if len(match) > 1 { 185 return match[1] 186 } 187 return "" 188 } 189 190 func extractNameAndVersion(nameVersion string) (name, version string) { 191 fields := strings.Split(nameVersion, "/") 192 name = fields[0] 193 if len(fields) > 1 { 194 version = fields[1] 195 } 196 return 197 } 198 199 func createNameAndVersion(name, version string) string { 200 return fmt.Sprintf("%s/%s", name, version) 201 } 202 203 func packageURL(m pkg.DotnetDepsEntry) string { 204 var qualifiers packageurl.Qualifiers 205 206 return packageurl.NewPackageURL( 207 // Although we use TypeNuget here due to historical reasons, note that it does not necessarily 208 // mean the package is a NuGet package. 209 packageurl.TypeNuget, 210 "", 211 m.Name, 212 m.Version, 213 qualifiers, 214 "", 215 ).ToString() 216 } 217 218 func newDotnetBinaryPackage(versionResources map[string]string, f file.Location) pkg.Package { 219 // TODO: we may decide to use the runtime information in the metadata, but that is not captured today 220 name, _ := findNameAndRuntimeFromVersionResources(versionResources) 221 222 if name == "" { 223 // older .NET runtime dlls may not have any version resources 224 name = strings.TrimSuffix(strings.TrimSuffix(path.Base(f.RealPath), ".exe"), ".dll") 225 } 226 227 ver := findVersionFromVersionResources(versionResources) 228 229 metadata := newDotnetPortableExecutableEntryFromMap(versionResources) 230 231 p := pkg.Package{ 232 Name: name, 233 Version: ver, 234 Locations: file.NewLocationSet(f.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), 235 Type: pkg.DotnetPkg, 236 Language: pkg.Dotnet, 237 PURL: binaryPackageURL(name, ver), 238 Metadata: metadata, 239 } 240 241 p.SetID() 242 243 return p 244 } 245 246 func binaryPackageURL(name, version string) string { 247 if name == "" { 248 return "" 249 } 250 return packageurl.NewPackageURL( 251 packageurl.TypeNuget, 252 "", 253 name, 254 version, 255 nil, 256 "", 257 ).ToString() 258 } 259 260 var binRuntimeSuffixPattern = regexp.MustCompile(`\s*\((?P<runtime>net[^)]*[0-9]+(\.[0-9]+)?)\)$`) 261 262 func findNameAndRuntimeFromVersionResources(versionResources map[string]string) (string, string) { 263 // PE files not authored by Microsoft tend to use ProductName as an identifier. 264 nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"} 265 266 if isMicrosoftVersionResource(versionResources) { 267 // for Microsoft files, prioritize FileDescription. 268 nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"} 269 } 270 271 var name string 272 for _, field := range nameFields { 273 value := spaceNormalize(versionResources[field]) 274 if value == "" { 275 continue 276 } 277 name = value 278 break 279 } 280 281 var runtime string 282 // look for indications of the runtime, such as "(net8.0)" or "(netstandard2.2)" suffixes 283 runtimes := binRuntimeSuffixPattern.FindStringSubmatch(name) 284 if len(runtimes) > 1 { 285 runtime = strings.TrimSpace(runtimes[1]) 286 name = strings.TrimSpace(strings.TrimSuffix(name, runtimes[0])) 287 } 288 289 return name, runtime 290 } 291 func isMicrosoftVersionResource(versionResources map[string]string) bool { 292 return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") || 293 strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft") 294 } 295 296 // spaceNormalize trims and normalizes whitespace in a string. 297 func spaceNormalize(value string) string { 298 value = strings.TrimSpace(value) 299 if value == "" { 300 return "" 301 } 302 // Ensure valid UTF-8. 303 value = strings.ToValidUTF8(value, "") 304 // Consolidate all whitespace. 305 value = spaceRegex.ReplaceAllString(value, " ") 306 // Remove non-printable characters. 307 value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "") 308 // Consolidate again and trim. 309 value = spaceRegex.ReplaceAllString(value, " ") 310 value = strings.TrimSpace(value) 311 return value 312 } 313 314 func findVersionFromVersionResources(versionResources map[string]string) string { 315 productVersion := extractVersionFromResourcesValue(versionResources["ProductVersion"]) 316 fileVersion := extractVersionFromResourcesValue(versionResources["FileVersion"]) 317 318 semanticVersionCompareResult := keepGreaterSemanticVersion(productVersion, fileVersion) 319 if semanticVersionCompareResult != "" { 320 return semanticVersionCompareResult 321 } 322 323 productVersionDetail := punctuationCount(productVersion) 324 fileVersionDetail := punctuationCount(fileVersion) 325 326 if containsNumber(productVersion) && productVersionDetail >= fileVersionDetail { 327 return productVersion 328 } 329 if containsNumber(fileVersion) && fileVersionDetail > 0 { 330 return fileVersion 331 } 332 if containsNumber(productVersion) { 333 return productVersion 334 } 335 if containsNumber(fileVersion) { 336 return fileVersion 337 } 338 339 return productVersion 340 } 341 342 func extractVersionFromResourcesValue(version string) string { 343 version = strings.TrimSpace(version) 344 out := "" 345 for i, f := range strings.Fields(version) { 346 if containsNumber(out) && !containsNumber(f) { 347 return out 348 } 349 if i == 0 { 350 out = f 351 } else { 352 out += " " + f 353 } 354 } 355 return out 356 } 357 358 func keepGreaterSemanticVersion(productVersion string, fileVersion string) string { 359 semanticProductVersion, err := version.NewVersion(productVersion) 360 if err != nil || semanticProductVersion == nil { 361 log.Tracef("Unable to create semantic version from product version %s", productVersion) 362 return "" 363 } 364 365 semanticFileVersion, err := version.NewVersion(fileVersion) 366 if err != nil || semanticFileVersion == nil { 367 log.Tracef("Unable to create semantic version from file version %s", fileVersion) 368 return productVersion 369 } 370 371 if semanticProductVersion.Equal(semanticFileVersion) { 372 return "" 373 } 374 if semanticFileVersion.GreaterThan(semanticProductVersion) { 375 return fileVersion 376 } 377 return productVersion 378 } 379 380 func containsNumber(s string) bool { 381 return numberRegex.MatchString(s) 382 } 383 384 func punctuationCount(s string) int { 385 return len(versionPunctuationRegex.FindAllString(s, -1)) 386 }