mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 07:56:36 +01:00
fix: restore case-insensitive java globbing for licenses, version and nested jars
The `archiveEntries` refactor dropped `GlobMatch`'s case-insensitive flag, so with nested cataloging off (the default) three lookups that main matched regardless of case became case-sensitive: - license files under `/META-INF/*` and `/*` (`meta-inf/LICENSE` was missed) - `/version.properties` - the `**/*.jar`, `**/*.war` check behind the "nested archives not cataloged" unknown, so `LIB.JAR` slipped past it `archiveEntries` now has a `globFold` for those call sites. The zip side restores `GlobMatch(true, ...)`, and the resolver side matches case-folded over every entry, since resolver globs are case-sensitive. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -46,7 +46,7 @@ func (z ZipFileManifest) Add(entry string, info os.FileInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GlobMatch returns the path keys to files (not directories) that match the given value(s).
|
// GlobMatch returns the path keys to files (not directories) that match the given value(s).
|
||||||
func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
func (z ZipFileManifest) GlobMatch(caseInsensitive bool, patterns ...string) []string {
|
||||||
uniqueMatches := strset.New()
|
uniqueMatches := strset.New()
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
@@ -58,7 +58,11 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
|||||||
|
|
||||||
// We want to match globs as if entries begin with a leading slash (akin to an absolute path)
|
// We want to match globs as if entries begin with a leading slash (akin to an absolute path)
|
||||||
// so that glob logic is consistent inside and outside of ZIP archives
|
// so that glob logic is consistent inside and outside of ZIP archives
|
||||||
normalizedEntry := normalizeZipEntryName(entry)
|
normalizedEntry := normalizeZipEntryName(caseInsensitive, entry)
|
||||||
|
|
||||||
|
if caseInsensitive {
|
||||||
|
pattern = strings.ToLower(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
matches, err := doublestar.Match(pattern, normalizedEntry)
|
matches, err := doublestar.Match(pattern, normalizedEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,7 +82,10 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
|
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
|
||||||
func normalizeZipEntryName(entry string) string {
|
func normalizeZipEntryName(caseInsensitive bool, entry string) string {
|
||||||
|
if caseInsensitive {
|
||||||
|
entry = strings.ToLower(entry)
|
||||||
|
}
|
||||||
if !strings.HasPrefix(entry, "/") {
|
if !strings.HasPrefix(entry, "/") {
|
||||||
return "/" + entry
|
return "/" + entry
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,10 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
|
|||||||
"**/a-file.txt",
|
"**/a-file.txt",
|
||||||
[]string{"some-dir/a-file.txt"},
|
[]string{"some-dir/a-file.txt"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"**/A-file.txt",
|
||||||
|
[]string{"some-dir/a-file.txt"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"**/*.zip",
|
"**/*.zip",
|
||||||
[]string{"nested.zip"},
|
[]string{"nested.zip"},
|
||||||
@@ -132,7 +136,7 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
|
|||||||
t.Run(tc.glob, func(t *testing.T) {
|
t.Run(tc.glob, func(t *testing.T) {
|
||||||
glob := tc.glob
|
glob := tc.glob
|
||||||
|
|
||||||
results := z.GlobMatch(glob)
|
results := z.GlobMatch(true, glob)
|
||||||
|
|
||||||
require.ElementsMatch(t, tc.expected, results)
|
require.ElementsMatch(t, tc.expected, results)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
"github.com/scylladb/go-set/strset"
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
@@ -21,6 +22,10 @@ type archiveEntries interface {
|
|||||||
// ("/META-INF/*"); the names returned have none.
|
// ("/META-INF/*"); the names returned have none.
|
||||||
glob(patterns ...string) []string
|
glob(patterns ...string) []string
|
||||||
|
|
||||||
|
// globFold is glob ignoring case, for names java tooling does not agree on the case of
|
||||||
|
// ("META-INF/LICENSE" vs "meta-inf/license", "X.JAR").
|
||||||
|
globFold(patterns ...string) []string
|
||||||
|
|
||||||
// contents reads the named entries, keyed by the same names.
|
// contents reads the named entries, keyed by the same names.
|
||||||
contents(ctx context.Context, names ...string) (map[string]string, error)
|
contents(ctx context.Context, names ...string) (map[string]string, error)
|
||||||
}
|
}
|
||||||
@@ -40,7 +45,11 @@ func newZipEntries(ctx context.Context, archivePath string) (*zipEntries, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (z *zipEntries) glob(patterns ...string) []string {
|
func (z *zipEntries) glob(patterns ...string) []string {
|
||||||
return z.fileManifest.GlobMatch(patterns...)
|
return z.fileManifest.GlobMatch(false, patterns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z *zipEntries) globFold(patterns ...string) []string {
|
||||||
|
return z.fileManifest.GlobMatch(true, patterns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (z *zipEntries) contents(ctx context.Context, names ...string) (map[string]string, error) {
|
func (z *zipEntries) contents(ctx context.Context, names ...string) (map[string]string, error) {
|
||||||
@@ -73,6 +82,31 @@ func (r *resolverEntries) glob(patterns ...string) []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// globFold walks every entry, since a resolver's globs are case-sensitive. The callers ask for a handful
|
||||||
|
// of names once per archive, so the walk is cheap next to cataloging the archive.
|
||||||
|
func (r *resolverEntries) globFold(patterns ...string) []string {
|
||||||
|
var folded []string
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
folded = append(folded, strings.ToLower(pattern))
|
||||||
|
}
|
||||||
|
seen := strset.New()
|
||||||
|
var names []string
|
||||||
|
for location := range r.resolver.AllLocations(context.Background()) {
|
||||||
|
name := strings.TrimPrefix(location.LocalPath(), "/")
|
||||||
|
if seen.Has(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, pattern := range folded {
|
||||||
|
if ok, _ := doublestar.Match(pattern, "/"+strings.ToLower(name)); ok {
|
||||||
|
seen.Add(name)
|
||||||
|
names = append(names, name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
func (r *resolverEntries) contents(_ context.Context, names ...string) (map[string]string, error) {
|
func (r *resolverEntries) contents(_ context.Context, names ...string) (map[string]string, error) {
|
||||||
out := make(map[string]string, len(names))
|
out := make(map[string]string, len(names))
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package java
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// java tooling doesn't agree on case ("meta-inf/LICENSE", "LIB.JAR"), and the license, version and
|
||||||
|
// nested-archive lookups have always matched regardless of it
|
||||||
|
func Test_archiveEntries_globFoldIgnoresCase(t *testing.T) {
|
||||||
|
names := []string{"meta-inf/LICENSE.txt", "Version.Properties", "lib/DEP.JAR", "META-INF/MANIFEST.MF"}
|
||||||
|
|
||||||
|
archivePath := filepath.Join(t.TempDir(), "app.jar")
|
||||||
|
f, err := os.Create(archivePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
zw := zip.NewWriter(f)
|
||||||
|
for _, name := range names {
|
||||||
|
_, err := zw.Create(name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.NoError(t, zw.Close())
|
||||||
|
require.NoError(t, f.Close())
|
||||||
|
|
||||||
|
fromZip, err := newZipEntries(context.Background(), archivePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var rooted []string
|
||||||
|
for _, name := range names {
|
||||||
|
rooted = append(rooted, "/"+name)
|
||||||
|
}
|
||||||
|
fromResolver := &resolverEntries{resolver: file.NewMockResolverForPaths(rooted...)}
|
||||||
|
|
||||||
|
for name, entries := range map[string]archiveEntries{"zip": fromZip, "resolver": fromResolver} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert.ElementsMatch(t, []string{"meta-inf/LICENSE.txt", "META-INF/MANIFEST.MF"}, entries.globFold("/META-INF/*"))
|
||||||
|
assert.Equal(t, []string{"Version.Properties"}, entries.globFold("/version.properties"))
|
||||||
|
assert.Equal(t, []string{"lib/DEP.JAR"}, entries.globFold("**/*.jar", "**/*.war"))
|
||||||
|
assert.Empty(t, entries.glob("/version.properties"), "glob itself stays case-sensitive")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -230,7 +230,7 @@ func (j *archiveParser) parse(ctx context.Context, parentPkg *pkg.Package) ([]pk
|
|||||||
} else if !archive.NestedCatalogingEnabled(ctx) {
|
} else if !archive.NestedCatalogingEnabled(ctx) {
|
||||||
// nothing will catalog these, so record them as unknowns
|
// nothing will catalog these, so record them as unknowns
|
||||||
// .jar and .war files are present in archives, are others? or generally just consider them top-level?
|
// .jar and .war files are present in archives, are others? or generally just consider them top-level?
|
||||||
nestedArchives := j.entries.glob("**/*.jar", "**/*.war")
|
nestedArchives := j.entries.globFold("**/*.jar", "**/*.war")
|
||||||
if len(nestedArchives) > 0 {
|
if len(nestedArchives) > 0 {
|
||||||
slices.Sort(nestedArchives)
|
slices.Sort(nestedArchives)
|
||||||
errs = unknown.Appendf(errs, j.location, "nested archives not cataloged: %v", strings.Join(nestedArchives, ", "))
|
errs = unknown.Appendf(errs, j.location, "nested archives not cataloged: %v", strings.Join(nestedArchives, ", "))
|
||||||
@@ -606,7 +606,7 @@ func (j *archiveParser) getLicenseFromFileInArchive(ctx context.Context) []pkg.L
|
|||||||
|
|
||||||
for _, glob := range []string{"/META-INF/*", "/*"} {
|
for _, glob := range []string{"/META-INF/*", "/*"} {
|
||||||
var licenseMatches []string
|
var licenseMatches []string
|
||||||
for _, f := range j.entries.glob(glob) {
|
for _, f := range j.entries.globFold(glob) {
|
||||||
if licenses.IsLicenseFile(path.Base(f)) {
|
if licenses.IsLicenseFile(path.Base(f)) {
|
||||||
licenseMatches = append(licenseMatches, f)
|
licenseMatches = append(licenseMatches, f)
|
||||||
}
|
}
|
||||||
@@ -657,7 +657,7 @@ func (j *archiveParser) versionFromPropertiesFile(ctx context.Context, manifest
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := j.entries.glob("/version.properties")
|
matches := j.entries.globFold("/version.properties")
|
||||||
if len(matches) != 1 {
|
if len(matches) != 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -700,7 +700,7 @@ func (j *archiveParser) discoverPkgsFromNestedArchives(ctx context.Context, pare
|
|||||||
// associating each discovered package to the given parent package.
|
// associating each discovered package to the given parent package.
|
||||||
func discoverPkgsFromZip(ctx context.Context, location file.Location, archivePath, contentPath string, fileManifest intFile.ZipFileManifest, parentPkg *pkg.Package, cfg ArchiveCatalogerConfig) ([]pkg.Package, []artifact.Relationship, error) {
|
func discoverPkgsFromZip(ctx context.Context, location file.Location, archivePath, contentPath string, fileManifest intFile.ZipFileManifest, parentPkg *pkg.Package, cfg ArchiveCatalogerConfig) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
// search and parse pom.properties files & fetch the contents
|
// search and parse pom.properties files & fetch the contents
|
||||||
openers, err := intFile.ExtractFromZipToUniqueTempFile(ctx, archivePath, contentPath, fileManifest.GlobMatch(archiveFormatGlobs...)...)
|
openers, err := intFile.ExtractFromZipToUniqueTempFile(ctx, archivePath, contentPath, fileManifest.GlobMatch(false, archiveFormatGlobs...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("unable to extract files from zip: %w", err)
|
return nil, nil, fmt.Errorf("unable to extract files from zip: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user