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  }