github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/java/parse_jvm_release.go (about) 1 package java 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "path" 9 "sort" 10 "strings" 11 12 "github.com/go-viper/mapstructure/v2" 13 14 "github.com/anchore/packageurl-go" 15 stereoFile "github.com/anchore/stereoscope/pkg/file" 16 "github.com/anchore/syft/internal/log" 17 "github.com/anchore/syft/syft/artifact" 18 "github.com/anchore/syft/syft/cpe" 19 "github.com/anchore/syft/syft/file" 20 "github.com/anchore/syft/syft/pkg" 21 "github.com/anchore/syft/syft/pkg/cataloger/generic" 22 ) 23 24 const ( 25 oracleVendor = "oracle" 26 openJdkProduct = "openjdk" 27 jre = "jre" 28 jdk = "jdk" 29 ) 30 31 // the /opt/java/openjdk/release file (and similar paths) is a file that is present in the multiple OpenJDK distributions 32 // here's an example of the contents of the file: 33 // 34 // IMPLEMENTOR="Eclipse Adoptium" 35 // IMPLEMENTOR_VERSION="Temurin-21.0.4+7" 36 // JAVA_RUNTIME_VERSION="21.0.4+7-LTS" 37 // JAVA_VERSION="21.0.4" 38 // JAVA_VERSION_DATE="2024-07-16" 39 // LIBC="gnu" 40 // MODULES="java.base java.compiler java.datatransfer java.xml java.prefs java.desktop java.instrument java.logging java.management java.security.sasl java.naming java.rmi java.management.rmi java.net.http java.scripting java.security.jgss java.transaction.xa java.sql java.sql.rowset java.xml.crypto java.se java.smartcardio jdk.accessibility jdk.internal.jvmstat jdk.attach jdk.charsets jdk.internal.opt jdk.zipfs jdk.compiler jdk.crypto.ec jdk.crypto.cryptoki jdk.dynalink jdk.internal.ed jdk.editpad jdk.hotspot.agent jdk.httpserver jdk.incubator.vector jdk.internal.le jdk.internal.vm.ci jdk.internal.vm.compiler jdk.internal.vm.compiler.management jdk.jartool jdk.javadoc jdk.jcmd jdk.management jdk.management.agent jdk.jconsole jdk.jdeps jdk.jdwp.agent jdk.jdi jdk.jfr jdk.jlink jdk.jpackage jdk.jshell jdk.jsobject jdk.jstatd jdk.localedata jdk.management.jfr jdk.naming.dns jdk.naming.rmi jdk.net jdk.nio.mapmode jdk.random jdk.sctp jdk.security.auth jdk.security.jgss jdk.unsupported jdk.unsupported.desktop jdk.xml.dom" 41 // OS_ARCH="aarch64" 42 // OS_NAME="Linux" 43 // SOURCE=".:git:13710926b798" 44 // BUILD_SOURCE="git:1271f10a26c47e1489a814dd2731f936a588d621" 45 // BUILD_SOURCE_REPO="https://github.com/adoptium/temurin-build.git" 46 // SOURCE_REPO="https://github.com/adoptium/jdk21u.git" 47 // FULL_VERSION="21.0.4+7-LTS" 48 // SEMANTIC_VERSION="21.0.4+7" 49 // BUILD_INFO="OS: Linux Version: 5.4.0-150-generic" 50 // JVM_VARIANT="Hotspot" 51 // JVM_VERSION="21.0.4+7-LTS" 52 // IMAGE_TYPE="JDK" 53 // 54 // In terms of the temurin flavor, these are controlled by: 55 // - config: https://github.com/adoptium/temurin-build/blob/v2023.01.03/sbin/common/config_init.sh 56 // - build script: https://github.com/adoptium/temurin-build/blob/v2023.01.03/sbin/build.sh#L1584-L1796 57 58 type jvmCpeInfo struct { 59 vendor, product, version string 60 } 61 62 func parseJVMRelease(_ context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 63 ri, err := parseJvmReleaseInfo(reader) 64 if err != nil { 65 return nil, nil, fmt.Errorf("unable to parse JVM release info %q: %w", reader.Path(), err) 66 } 67 68 if ri == nil { 69 // TODO: known-unknown: expected JDK installation package 70 return nil, nil, nil 71 } 72 73 version := jvmPackageVersion(ri) 74 // TODO: detect old and new version format from multiple fields 75 76 licenses := jvmLicenses(resolver, ri) 77 78 locations := file.NewLocationSet(reader.Location) 79 80 for _, lic := range licenses.ToSlice() { 81 locations.Add(lic.Locations.ToSlice()...) 82 } 83 84 installDir := path.Dir(reader.Path()) 85 files, hasJdk := findJvmFiles(resolver, installDir) 86 87 // the reason we use the reference to get the real path is in cases where the file cataloger is involved 88 // (thus the real path is not available except for in the original file reference). This is important 89 // since the path is critical for distinguishing between different JVM vendors. 90 vendor, product := jvmPrimaryVendorProduct(ri, string(reader.Reference().RealPath), hasJdk) 91 92 p := pkg.Package{ 93 Name: product, 94 Locations: locations, 95 Version: version, 96 CPEs: jvmCpes(version, vendor, product, ri.ImageType, hasJdk), 97 PURL: jvmPurl(*ri, version, vendor, product), 98 Licenses: licenses, 99 Type: pkg.BinaryPkg, 100 Metadata: pkg.JavaVMInstallation{ 101 Release: *ri, 102 Files: files, 103 }, 104 } 105 p.SetID() 106 107 return []pkg.Package{p}, nil, nil 108 } 109 110 func jvmLicenses(_ file.Resolver, _ *pkg.JavaVMRelease) pkg.LicenseSet { 111 // TODO: get this from the dir(<RELEASE>)/legal/**/LICENSE files when we start cataloging license content 112 // see https://github.com/anchore/syft/issues/656 113 return pkg.NewLicenseSet() 114 } 115 116 func findJvmFiles(resolver file.Resolver, installDir string) ([]string, bool) { 117 ownedLocations, err := resolver.FilesByGlob(installDir + "/**") 118 if err != nil { 119 // TODO: known-unknowns 120 log.WithFields("path", installDir, "error", err).Trace("unable to find installed JVM files") 121 } 122 123 var results []string 124 var hasJdk bool 125 for _, loc := range ownedLocations { 126 p := loc.Path() 127 results = append(results, p) 128 if !hasJdk && strings.HasSuffix(p, "bin/javac") { 129 hasJdk = true 130 } 131 } 132 133 sort.Strings(results) 134 135 return results, hasJdk 136 } 137 138 func jvmPurl(ri pkg.JavaVMRelease, version, vendor, product string) string { 139 var qualifiers []packageurl.Qualifier 140 if ri.SourceRepo != "" { 141 qualifiers = append(qualifiers, packageurl.Qualifier{ 142 Key: "repository_url", 143 Value: ri.SourceRepo, 144 }) 145 } else if ri.BuildSourceRepo != "" { 146 qualifiers = append(qualifiers, packageurl.Qualifier{ 147 Key: "repository_url", 148 Value: ri.BuildSourceRepo, 149 }) 150 } 151 152 pURL := packageurl.NewPackageURL( 153 packageurl.TypeGeneric, 154 vendor, 155 product, 156 version, 157 qualifiers, 158 "") 159 return pURL.ToString() 160 } 161 162 func jvmPrimaryVendorProduct(ri *pkg.JavaVMRelease, path string, hasJdk bool) (string, string) { 163 implementor := strings.ReplaceAll(strings.ToLower(ri.Implementor), " ", "") 164 165 pickProduct := func() string { 166 if hasJdk || jvmProjectByType(ri.ImageType) == jdk { 167 return jdk 168 } 169 return jre 170 } 171 172 switch { 173 case strings.Contains(implementor, "azul") || strings.Contains(path, "zulu"): 174 return "azul", "zulu" 175 176 case strings.Contains(implementor, "sun"): 177 return "sun", pickProduct() 178 179 case strings.Contains(implementor, "ibm") || strings.Contains(path, "/ibm"): 180 if hasJdk { 181 return "ibm", "java_sdk" 182 } 183 return "ibm", "java" 184 185 case strings.Contains(implementor, "oracle") || strings.Contains(path, "oracle") || strings.Contains(ri.BuildType, "commercial"): 186 return oracleVendor, pickProduct() 187 } 188 return oracleVendor, openJdkProduct 189 } 190 191 func jvmCpes(version, primaryVendor, primaryProduct, imageType string, hasJdk bool) []cpe.CPE { 192 // see https://github.com/anchore/syft/issues/2422 for more context 193 194 var candidates []jvmCpeInfo 195 196 newCandidate := func(ven, prod, ver string) { 197 candidates = append(candidates, jvmCpeInfo{ 198 vendor: ven, 199 product: prod, 200 version: ver, 201 }) 202 } 203 204 newEnterpriseCandidate := func(ven, ver string) { 205 newCandidate(ven, jre, ver) 206 if hasJdk || jvmProjectByType(imageType) == jdk { 207 newCandidate(ven, jdk, ver) 208 } 209 } 210 211 switch { 212 case primaryVendor == "azul": 213 newCandidate(primaryVendor, "zulu", version) 214 newCandidate(oracleVendor, openJdkProduct, version) 215 216 case primaryVendor == "sun": 217 newEnterpriseCandidate(primaryVendor, version) 218 219 case primaryVendor == oracleVendor && primaryProduct != openJdkProduct: 220 newCandidate(primaryVendor, "java_se", version) 221 newEnterpriseCandidate(primaryVendor, version) 222 default: 223 newCandidate(primaryVendor, primaryProduct, version) 224 } 225 226 var cpes []cpe.CPE 227 for _, candidate := range candidates { 228 c := newJvmCpe(candidate) 229 if c == nil { 230 continue 231 } 232 cpes = append(cpes, *c) 233 } 234 235 return cpes 236 } 237 238 func getJVMVersionAndUpdate(version string) (string, string) { 239 hasPlus := strings.Contains(version, "+") 240 hasUnderscore := strings.Contains(version, "_") 241 242 switch { 243 case hasUnderscore: 244 // assume legacy version strings are provided 245 // example: 1.8.0_302-b08 246 fields := strings.Split(version, "_") 247 if len(fields) == 2 { 248 shortVer := fields[0] 249 fields = strings.Split(fields[1], "-") 250 return shortVer, fields[0] 251 } 252 case hasPlus: 253 // assume JEP 223 version strings are provided 254 // example: 9.0.1+20 255 fields := strings.Split(version, "+") 256 return fields[0], "" 257 } 258 259 // this could be a legacy or modern string that does not have an update 260 return version, "" 261 } 262 263 func newJvmCpe(candidate jvmCpeInfo) *cpe.CPE { 264 if candidate.vendor == "" || candidate.product == "" || candidate.version == "" { 265 return nil 266 } 267 268 shortVer, update := getJVMVersionAndUpdate(candidate.version) 269 270 if shortVer == "" { 271 return nil 272 } 273 274 if update != "" && !strings.Contains(strings.ToLower(update), "update") { 275 update = "update" + trim0sFromLeft(update) 276 } 277 278 return &cpe.CPE{ 279 Attributes: cpe.Attributes{ 280 Part: "a", 281 Vendor: candidate.vendor, 282 Product: candidate.product, 283 Version: shortVer, 284 Update: update, 285 }, 286 // note: we must use a declared source here. Though we are not directly raising up raw CPEs from cataloged material, 287 // these are vastly more reliable and accurate than what would be generated from the cpe generator logic. 288 // We want these CPEs to override any generated CPEs (and in fact prevent the generation of CPEs for these packages altogether). 289 Source: cpe.DeclaredSource, 290 } 291 } 292 293 func jvmProjectByType(ty string) string { 294 if strings.Contains(strings.ToLower(ty), jre) { 295 return jre 296 } 297 return jdk 298 } 299 300 // jvmPackageVersion attempts to extract the correct version value for the JVM given a platter of version strings to choose 301 // from, and makes special consideration to what a valid version is relative to JEP 223. 302 // 303 // example version values (openjdk >8): 304 // 305 // IMPLEMENTOR_VERSION "Temurin-21.0.4+7" 306 // JAVA_RUNTIME_VERSION "21.0.4+7-LTS" 307 // FULL_VERSION "21.0.4+7-LTS" 308 // SEMANTIC_VERSION "21.0.4+7" 309 // JAVA_VERSION "21.0.4" 310 // 311 // example version values (openjdk 8): 312 // 313 // JAVA_VERSION "1.8.0_422" 314 // FULL_VERSION "1.8.0_422-b05" 315 // SEMANTIC_VERSION "8.0.422+5" 316 // 317 // example version values (openjdk 8, but older): 318 // 319 // JAVA_VERSION "1.8.0_302" 320 // FULL_VERSION "1.8.0_302-b08" 321 // SEMANTIC_VERSION "8.0.302+8" 322 // 323 // example version values (oracle): 324 // 325 // IMPLEMENTOR_VERSION (missing) 326 // JAVA_RUNTIME_VERSION "22.0.2+9-70" 327 // JAVA_VERSION "22.0.2" 328 // 329 // example version values (mariner): 330 // 331 // IMPLEMENTOR_VERSION "Microsoft-9889599" 332 // JAVA_RUNTIME_VERSION "17.0.12+7-LTS" 333 // JAVA_VERSION "17.0.12" 334 // 335 // example version values (amazon): 336 // 337 // IMPLEMENTOR_VERSION "Corretto-17.0.12.7.1" 338 // JAVA_RUNTIME_VERSION "17.0.12+7-LTS" 339 // JAVA_VERSION "17.0.12" 340 // 341 // JEP 223 changes to JVM version string in the following way: 342 // 343 // Pre JEP 223 Post JEP 223 344 // Release Type long short long short 345 // ------------ -------------------- -------------------- 346 // Early Access 1.9.0-ea-b19 9-ea 9-ea+19 9-ea 347 // Major 1.9.0-b100 9 9+100 9 348 // Security #1 1.9.0_5-b20 9u5 9.0.1+20 9.0.1 349 // Security #2 1.9.0_11-b12 9u11 9.0.2+12 9.0.2 350 // Minor #1 1.9.0_20-b62 9u20 9.1.2+62 9.1.2 351 // Security #3 1.9.0_25-b15 9u25 9.1.3+15 9.1.3 352 // Security #4 1.9.0_31-b08 9u31 9.1.4+8 9.1.4 353 // Minor #2 1.9.0_40-b45 9u40 9.2.4+45 9.2.4 354 // 355 // What does this mean for us? In terms of the version selected, use semver-compliant strings when available. 356 // 357 // In terms of where to get the version: 358 // 359 // SEMANTIC_VERSION Reasonably prevalent, but most accurate in terms of comparable versions 360 // JAVA_RUNTIME_VERSION Reasonable prevalent, but difficult to distinguish pre-release info vs aux info (jep 223 sensitive) 361 // FULL_VERSION Reasonable prevalent, but difficult to distinguish pre-release info vs aux info (jep 223 sensitive) 362 // JAVA_VERSION Most prevalent, but least specific (jep 223 sensitive) 363 // IMPLEMENTOR_VERSION Unusable or missing in some cases 364 func jvmPackageVersion(ri *pkg.JavaVMRelease) string { 365 var version string 366 switch { 367 case ri.JavaRuntimeVersion != "": 368 return ri.JavaRuntimeVersion 369 case ri.FullVersion != "": 370 // if the full version major version matches the java version major version, then use the full version 371 fullMajor := strings.Split(ri.FullVersion, ".")[0] 372 javaMajor := strings.Split(ri.JavaVersion, ".")[0] 373 if fullMajor == javaMajor { 374 return ri.FullVersion 375 } 376 fallthrough 377 case ri.JavaVersion != "": 378 return ri.JavaVersion 379 } 380 381 return version 382 } 383 384 func trim0sFromLeft(v string) string { 385 if v == "0" { 386 return v 387 } 388 return strings.TrimLeft(v, "0") 389 } 390 391 func parseJvmReleaseInfo(r io.ReadCloser) (*pkg.JavaVMRelease, error) { 392 defer r.Close() 393 394 data := make(map[string]any) 395 scanner := bufio.NewScanner(io.LimitReader(r, 500*stereoFile.KB)) 396 397 for scanner.Scan() { 398 line := scanner.Text() 399 parts := strings.SplitN(line, "=", 2) 400 if len(parts) != 2 { 401 continue 402 } 403 key := parts[0] 404 value := strings.Trim(parts[1], `"`) 405 406 if key == "MODULES" { 407 data[key] = strings.Split(value, " ") 408 } else { 409 data[key] = value 410 } 411 } 412 413 if err := scanner.Err(); err != nil { 414 return nil, err 415 } 416 417 // if we're missing key fields, then we don't have a JVM release file 418 if data["JAVA_VERSION"] == nil && data["JAVA_RUNTIME_VERSION"] == nil { 419 return nil, nil 420 } 421 422 var ri pkg.JavaVMRelease 423 if err := mapstructure.Decode(data, &ri); err != nil { 424 return nil, err 425 } 426 427 return &ri, nil 428 }