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" "iter"
"os" "os"
"path" "path"
"regexp"
"slices" "slices"
"strings" "strings"
@@ -301,6 +302,9 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest
if version == "" { if version == "" {
version = selectVersion(manifest, j.fileInfo) version = selectVersion(manifest, j.fileInfo)
} }
if version == "" {
version = j.versionFromPropertiesFile(ctx, manifest)
}
if len(lics) == 0 { if len(lics) == 0 {
fileLicenses := j.getLicenseFromFileInArchive(ctx) fileLicenses := j.getLicenseFromFileInArchive(ctx)
@@ -609,6 +613,44 @@ func (j *archiveParser) getLicenseFromFileInArchive(ctx context.Context) []pkg.L
return identified 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) { 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 // 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) 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 ORG_MULTIPLE_THENAME = org.multiple-thename
MICRONAUT_AOP = micronaut-aop-4.9.11 MICRONAUT_AOP = micronaut-aop-4.9.11
COMMONS_LANG3 = commons-lang3-3.12.0 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 .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 # requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE) 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: $(CACHE_DIR)/$(JACKSON_CORE).jar:
mkdir -p $(CACHE_DIR) mkdir -p $(CACHE_DIR)
@@ -62,6 +65,18 @@ $(CACHE_DIR)/$(COMMONS_LANG3).jar:
mkdir -p $(CACHE_DIR) mkdir -p $(CACHE_DIR)
cd $(COMMONS_LANG3) && zip -r $(CACHE_PATH)/$(COMMONS_LANG3).jar . 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, # 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 # so it is important to not include it in the generated test fixture
$(CACHE_DIR)/gradle.hpi: $(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)
})
}
}