fixed java-archive-cataloger returns unknown ver for uber-jars , fixed #5163 (#5164)

Signed-off-by: Rez Moss <hi@rezmoss.com>
This commit is contained in:
Rez Moss
2026-09-03 16:09:20 -04:00
committed by GitHub
parent 77031752fa
commit c935b71c9e
9 changed files with 201 additions and 1 deletions
+42
View File
@@ -9,6 +9,7 @@ import (
"iter"
"os"
"path"
"regexp"
"slices"
"strings"
@@ -301,6 +302,9 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest
if version == "" {
version = selectVersion(manifest, j.fileInfo)
}
if version == "" {
version = j.versionFromPropertiesFile(ctx, manifest)
}
if len(lics) == 0 {
fileLicenses := j.getLicenseFromFileInArchive(ctx)
@@ -609,6 +613,44 @@ func (j *archiveParser) getLicenseFromFileInArchive(ctx context.Context) []pkg.L
return identified
}
// optional "v" + leading digit + version-ish chars; rejects placeholders like "${revision}"
var releaseVersionPattern = regexp.MustCompile(`^v?(\d[0-9A-Za-z.\-_+]*)$`)
// versionFromPropertiesFile: last-resort ver from root version.properties for uber-jars built
// outside maven/gradle e.g. metabase, "tag=v0.63.5", shading can land a deps file at t same
// path, so be conservative => Main-Class jars only, "version"/"tag" keys only, version shaped values only
func (j *archiveParser) versionFromPropertiesFile(ctx context.Context, manifest *pkg.JavaManifest) string {
if manifest == nil || manifest.Main.MustGet("Main-Class") == "" {
return ""
}
matches := j.fileManifest.GlobMatch(true, "/version.properties")
if len(matches) != 1 {
return ""
}
contents, err := intFile.ContentsFromZip(ctx, j.archivePath, matches...)
if err != nil {
log.Debugf("unable to extract version.properties (%s): %v", j.location, err)
return ""
}
props := map[string]string{}
for line := range strings.Lines(contents[matches[0]]) {
if k, v, ok := strings.Cut(line, "="); ok {
props[strings.ToLower(strings.TrimSpace(k))] = strings.TrimSpace(v)
}
}
for _, key := range []string{"version", "tag"} {
if m := releaseVersionPattern.FindStringSubmatch(props[key]); m != nil {
return m[1]
}
}
return ""
}
func (j *archiveParser) discoverPkgsFromNestedArchives(ctx context.Context, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
// we know that all java archives are zip formatted files, so we can use the shared zip helper
return discoverPkgsFromZip(ctx, j.location, j.archivePath, j.contentPath, j.fileManifest, parentPkg, j.cfg)
+16 -1
View File
@@ -15,6 +15,9 @@ MULTIPLE_MATCHING = multiple-matching-2.11.5
ORG_MULTIPLE_THENAME = org.multiple-thename
MICRONAUT_AOP = micronaut-aop-4.9.11
COMMONS_LANG3 = commons-lang3-3.12.0
UBER_APP_VERSION_PROPERTIES = uber-app-version-properties
UBER_APP_VERSION_PLACEHOLDER = uber-app-version-placeholder
LIB_WITH_VERSION_PROPERTIES = lib-with-version-properties
.DEFAULT_GOAL := fixtures
@@ -24,7 +27,7 @@ fixtures: $(CACHE_DIR)
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar $(CACHE_DIR)/$(COMMONS_LANG3).jar
$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar $(CACHE_DIR)/$(COMMONS_LANG3).jar $(CACHE_DIR)/$(UBER_APP_VERSION_PROPERTIES).jar $(CACHE_DIR)/$(UBER_APP_VERSION_PLACEHOLDER).jar $(CACHE_DIR)/$(LIB_WITH_VERSION_PROPERTIES).jar
$(CACHE_DIR)/$(JACKSON_CORE).jar:
mkdir -p $(CACHE_DIR)
@@ -62,6 +65,18 @@ $(CACHE_DIR)/$(COMMONS_LANG3).jar:
mkdir -p $(CACHE_DIR)
cd $(COMMONS_LANG3) && zip -r $(CACHE_PATH)/$(COMMONS_LANG3).jar .
$(CACHE_DIR)/$(UBER_APP_VERSION_PROPERTIES).jar:
mkdir -p $(CACHE_DIR)
cd $(UBER_APP_VERSION_PROPERTIES) && zip -r $(CACHE_PATH)/$(UBER_APP_VERSION_PROPERTIES).jar .
$(CACHE_DIR)/$(UBER_APP_VERSION_PLACEHOLDER).jar:
mkdir -p $(CACHE_DIR)
cd $(UBER_APP_VERSION_PLACEHOLDER) && zip -r $(CACHE_PATH)/$(UBER_APP_VERSION_PLACEHOLDER).jar .
$(CACHE_DIR)/$(LIB_WITH_VERSION_PROPERTIES).jar:
mkdir -p $(CACHE_DIR)
cd $(LIB_WITH_VERSION_PROPERTIES) && zip -r $(CACHE_PATH)/$(LIB_WITH_VERSION_PROPERTIES).jar .
# Jenkins plugins typically do not have the version included in the archive name,
# so it is important to not include it in the generated test fixture
$(CACHE_DIR)/gradle.hpi:
@@ -0,0 +1,3 @@
Manifest-Version: 1.0
Created-By: Apache Maven
@@ -0,0 +1 @@
version=1.2.3
@@ -0,0 +1,4 @@
Manifest-Version: 1.0
Created-By: build.clj
Main-Class: example.core.bootstrap
@@ -0,0 +1,2 @@
version=${revision}
build.date=${build_timestamp}
@@ -0,0 +1,6 @@
Manifest-Version: 1.0
Multi-Release: true
Created-By: build.clj
Build-Jdk-Spec: 25
Main-Class: example.core.bootstrap
@@ -0,0 +1,3 @@
tag=v0.63.5
hash=1dca717
date=2026-08-05
@@ -0,0 +1,124 @@
package java
import (
"context"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func readManifest(t *testing.T, ctx context.Context, parser *archiveParser) *pkg.JavaManifest {
t.Helper()
matches := parser.fileManifest.GlobMatch(false, manifestGlob)
require.Len(t, matches, 1)
contents, err := intFile.ContentsFromZip(ctx, parser.archivePath, matches...)
require.NoError(t, err)
manifest, err := parseJavaManifest(parser.archivePath, strings.NewReader(contents[matches[0]]))
require.NoError(t, err)
return manifest
}
func Test_archiveParser_versionFromPropertiesFile(t *testing.T) {
tests := []struct {
name string
fixture string
want string
}{
{
// "tag=v0.63.5"
name: "release tag from root version.properties",
fixture: "uber-app-version-properties",
want: "0.63.5",
},
{
// clickhouse-jdbc ships "version=${revision}" must not become a version
name: "unresolved build placeholder is rejected",
fixture: "uber-app-version-placeholder",
want: "",
},
{
// no Main-Class -> cant tell app uberjar from bundled dep
name: "library without Main-Class is rejected",
fixture: "lib-with-version-properties",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := pkgtest.Context(t)
fixturePath := generateJavaMetadataJarFixture(t, tt.fixture, "jar")
fixture, err := os.Open(fixturePath)
require.NoError(t, err)
parser, cleanupFn, err := newJavaArchiveParser(ctx,
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
}, false, DefaultArchiveCatalogerConfig())
defer cleanupFn()
require.NoError(t, err)
assert.Equal(t, tt.want, parser.versionFromPropertiesFile(ctx, readManifest(t, ctx, parser)))
})
}
}
func Test_archiveParser_versionFromPropertiesFile_noManifest(t *testing.T) {
ctx := pkgtest.Context(t)
fixturePath := generateJavaMetadataJarFixture(t, "uber-app-version-properties", "jar")
fixture, err := os.Open(fixturePath)
require.NoError(t, err)
parser, cleanupFn, err := newJavaArchiveParser(ctx,
file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
}, false, DefaultArchiveCatalogerConfig())
defer cleanupFn()
require.NoError(t, err)
assert.Empty(t, parser.versionFromPropertiesFile(ctx, nil))
}
func Test_releaseVersionPattern(t *testing.T) {
tests := []struct {
value string
want string
}{
{"v0.63.5", "0.63.5"},
{"0.63.5", "0.63.5"},
{"2.5.1", "2.5.1"},
{"1.0.0-SNAPSHOT", "1.0.0-SNAPSHOT"},
{"3", "3"},
{"${revision}", ""},
{"trunk-nightly build", ""},
{"valhalla", ""},
{"", ""},
{"v", ""},
}
for _, tt := range tests {
t.Run(tt.value, func(t *testing.T) {
var got string
if m := releaseVersionPattern.FindStringSubmatch(tt.value); m != nil {
got = m[1]
}
assert.Equal(t, tt.want, got)
})
}
}