mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
Fix hang when scanning a Java resource adapter (.rar) file (#5348)
* fix: scan a Java resource adapter (.rar) given as a file source The file source picks an extractor for the scan target from its file name alone. A Java resource adapter archive (JCA) uses the .rar extension but is a zip file, so it was handed to the RAR extractor. With rardecode v2.2.0 the RAR extractor does not return on non-RAR input (an infinite loop in its signature search), so `syft scan --from file adapter.rar` never finished. The same bytes named .jar scan in under a second. When the name says RAR but the content is a zip archive, leave the file unextracted, the same way .jar, .war and .ear scan targets are handled. The java cataloger already reads .rar files (anchore/syft#4136) and now finds the resource adapter and its nested jars. Real RAR archives are still extracted. Signed-off-by: Ismael Donmez <ismael.donmez@chainguard.dev> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(deps): bump github.com/nwaples/rardecode/v2 from 2.2.0 to 2.4.1 rardecode v2.2.0 loops forever in its RAR signature search when the input does not start with a RAR signature (fixed upstream in nwaples/rardecode f501ac8, "fix infinite loop in findSig", released in v2.2.2). syft reaches that search for any file source whose name contains ".rar", so a scan of a self-extracting RAR, or of any other file with such a name, never finished. With v2.4.1 a self-extracting RAR is extracted, and a file that is not a RAR archive fails fast with "rardecode: RAR signature not found", the same way a file named .zip that is not a zip archive fails today. github.com/mholt/archives v0.1.5 (the latest release) still requires v2.2.0, so the newer version is selected here. Signed-off-by: Ismael Donmez <ismael.donmez@chainguard.dev> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: clarify the .rar zip log line and cover self-extracting RAR sources - the debug line no longer calls every `.rar`-named zip a resource adapter - a self-extracting RAR (stub before the signature) given as a file source is still extracted Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Ismael Donmez <ismael.donmez@chainguard.dev> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Alex Goodman
parent
f21e439a63
commit
0bbcff1ae3
@@ -248,7 +248,7 @@ require (
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.4.1 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.6 // indirect
|
||||
|
||||
@@ -758,8 +758,8 @@ github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 h1:kpt9ZfKcm+
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1/go.mod h1:qgCw4bBKZX8qMgGeEZzGFVT3notl42dBjNqO2jut0M0=
|
||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
|
||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
||||
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/nwaples/rardecode/v2 v2.4.1 h1:F7zNW2LdAuuBThHWXQaiFUGVD/sef299NfWSB1nHAl4=
|
||||
github.com/nwaples/rardecode/v2 v2.4.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
|
||||
@@ -214,6 +214,11 @@ func fileAnalysisPath(path string, skipExtractArchive bool) (string, func() erro
|
||||
|
||||
envelopedUnarchiver, _, err := intFile.IdentifyArchive(context.Background(), path, nil)
|
||||
if unarchiver, ok := envelopedUnarchiver.(archives.Extractor); err == nil && ok {
|
||||
if isZipWithRarName(path, envelopedUnarchiver) {
|
||||
log.Debugf("source path has a .rar name but zip content (e.g. a Java resource adapter archive); analyzing it as a single file")
|
||||
return analysisPath, cleanupFn, nil
|
||||
}
|
||||
|
||||
analysisPath, cleanupFn, err = unarchiveToTmp(path, unarchiver)
|
||||
if err != nil {
|
||||
return "", cleanupFn, fmt.Errorf("unable to unarchive source file: %w", err)
|
||||
@@ -225,6 +230,25 @@ func fileAnalysisPath(path string, skipExtractArchive bool) (string, func() erro
|
||||
return analysisPath, cleanupFn, nil
|
||||
}
|
||||
|
||||
// isZipWithRarName reports whether the archive format was identified as RAR from the file name while the file
|
||||
// content is a zip archive. Archive identification above uses the file name only, and ".rar" is also the extension
|
||||
// of Java resource adapter archives (JCA), which are zip files. Such a file is left for the java cataloger to read,
|
||||
// the same way .jar, .war and .ear files are, instead of being handed to the RAR extractor, which cannot read it.
|
||||
func isZipWithRarName(path string, format archives.Format) bool {
|
||||
if _, ok := format.(archives.Rar); !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
match, err := archives.Zip{}.Match(context.Background(), "", f)
|
||||
return err == nil && match.ByStream
|
||||
}
|
||||
|
||||
func digestOfFileContents(path string) string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package filesource
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -173,6 +175,123 @@ func TestNewFromFile_WithArchive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromFile_RarExtension(t *testing.T) {
|
||||
testutil.Chdir(t, "..") // run with source/testdata
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input string
|
||||
wantExtracted bool
|
||||
wantPaths []string
|
||||
wantNoPaths []string
|
||||
}{
|
||||
{
|
||||
// a Java resource adapter archive is a zip file with a .rar extension: it must not be handed to the RAR
|
||||
// extractor, but left for the java cataloger to read as a single file (like a .jar)
|
||||
desc: "zip archive with a .rar name is analyzed as a single file",
|
||||
input: createResourceAdapterArchive(t, t.TempDir()),
|
||||
wantExtracted: false,
|
||||
wantPaths: []string{"example-ra-1.0.0.rar"},
|
||||
wantNoPaths: []string{"META-INF/ra.xml", "example-lib-1.0.0.jar"},
|
||||
},
|
||||
{
|
||||
// a self-extracting RAR has an executable stub before the RAR signature. The stub holds no "R" bytes,
|
||||
// which is the input that made rardecode v2.2.0 loop forever in its signature search
|
||||
desc: "self-extracting RAR archive is extracted",
|
||||
input: writeSfxRar(t, t.TempDir()),
|
||||
wantExtracted: true,
|
||||
wantPaths: []string{"inside.txt"},
|
||||
wantNoPaths: []string{"example-sfx.rar"},
|
||||
},
|
||||
{
|
||||
// a RAR 5.0 archive holding one stored file (inside.txt)
|
||||
desc: "RAR archive is extracted",
|
||||
input: "testdata/rar-archive/example.rar",
|
||||
wantExtracted: true,
|
||||
wantPaths: []string{"inside.txt"},
|
||||
wantNoPaths: []string{"example.rar"},
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
src, err := New(Config{
|
||||
Path: test.input,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, src.Close())
|
||||
})
|
||||
|
||||
assert.Equal(t, test.wantExtracted, src.(*fileSource).analysisPath != test.input)
|
||||
|
||||
res, err := src.FileResolver(source.SquashedScope)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, p := range test.wantPaths {
|
||||
refs, err := res.FilesByPath(p)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, refs, 1, "expected to find %q", p)
|
||||
}
|
||||
|
||||
for _, p := range test.wantNoPaths {
|
||||
refs, err := res.FilesByPath(p)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, refs, "expected not to find %q", p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// createResourceAdapterArchive writes a minimal Java resource adapter archive (a zip file with a .rar extension that
|
||||
// holds a deployment descriptor and a nested jar) into dir and returns its path.
|
||||
func createResourceAdapterArchive(t testing.TB, dir string) string {
|
||||
t.Helper()
|
||||
|
||||
lib := zipBytes(t, [][2]string{
|
||||
{"META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"},
|
||||
{"META-INF/maven/org.example/example-lib/pom.properties", "groupId=org.example\nartifactId=example-lib\nversion=1.0.0\n"},
|
||||
})
|
||||
|
||||
ra := zipBytes(t, [][2]string{
|
||||
{"META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"},
|
||||
{"META-INF/ra.xml", "<connector/>\n"},
|
||||
{"example-lib-1.0.0.jar", string(lib)},
|
||||
{"META-INF/maven/org.example/example-ra/pom.properties", "groupId=org.example\nartifactId=example-ra\nversion=1.0.0\n"},
|
||||
})
|
||||
|
||||
archivePath := filepath.Join(dir, "example-ra-1.0.0.rar")
|
||||
require.NoError(t, os.WriteFile(archivePath, ra, 0o600))
|
||||
return archivePath
|
||||
}
|
||||
|
||||
// writeSfxRar writes testdata/rar-archive/example.rar behind a stand-in executable stub into dir and returns its path.
|
||||
func writeSfxRar(t testing.TB, dir string) string {
|
||||
t.Helper()
|
||||
|
||||
rar, err := os.ReadFile("testdata/rar-archive/example.rar")
|
||||
require.NoError(t, err)
|
||||
|
||||
sfxPath := filepath.Join(dir, "example-sfx.rar")
|
||||
require.NoError(t, os.WriteFile(sfxPath, append(bytes.Repeat([]byte("MZ stub "), 1024), rar...), 0o600))
|
||||
return sfxPath
|
||||
}
|
||||
|
||||
// zipBytes returns a zip archive holding the given (name, contents) entries, in order.
|
||||
func zipBytes(t testing.TB, entries [][2]string) []byte {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
for _, e := range entries {
|
||||
f, err := w.Create(e[0])
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write([]byte(e[1]))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, w.Close())
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function,
|
||||
// which should be called (typically deferred) by the caller, the path of the created tar archive, and an error,
|
||||
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
|
||||
|
||||
BIN
Binary file not shown.
@@ -2,6 +2,8 @@ package cli
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -31,6 +33,25 @@ func TestArchiveScan(t *testing.T) {
|
||||
assertPackageCount(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
// a Java resource adapter archive is a zip file with a .rar extension
|
||||
name: "scan a java resource adapter archive",
|
||||
args: []string{
|
||||
"scan",
|
||||
"-o",
|
||||
"json",
|
||||
"--from",
|
||||
"file",
|
||||
createResourceAdapterArchive(t, t.TempDir()),
|
||||
},
|
||||
assertions: []traitAssertion{
|
||||
assertSuccessfulReturnCode,
|
||||
assertJsonReport,
|
||||
assertInOutput("pkg:maven/org.example/example-ra@1.0.0"),
|
||||
assertInOutput("pkg:maven/org.example/example-lib@1.0.0"),
|
||||
assertPackageCount(2),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -58,3 +79,37 @@ func createArchive(t *testing.T, path string, destDir string) string {
|
||||
|
||||
return archivePath
|
||||
}
|
||||
|
||||
// createResourceAdapterArchive writes a minimal Java resource adapter archive (a zip file with a .rar extension that
|
||||
// holds a deployment descriptor and a nested jar) into destDir and returns its path.
|
||||
func createResourceAdapterArchive(t *testing.T, destDir string) string {
|
||||
lib := zipBytes(t, [][2]string{
|
||||
{"META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"},
|
||||
{"META-INF/maven/org.example/example-lib/pom.properties", "groupId=org.example\nartifactId=example-lib\nversion=1.0.0\n"},
|
||||
})
|
||||
|
||||
ra := zipBytes(t, [][2]string{
|
||||
{"META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"},
|
||||
{"META-INF/ra.xml", "<connector/>\n"},
|
||||
{"example-lib-1.0.0.jar", string(lib)},
|
||||
{"META-INF/maven/org.example/example-ra/pom.properties", "groupId=org.example\nartifactId=example-ra\nversion=1.0.0\n"},
|
||||
})
|
||||
|
||||
archivePath := filepath.Join(destDir, "example-ra-1.0.0.rar")
|
||||
require.NoError(t, os.WriteFile(archivePath, ra, 0o600))
|
||||
return archivePath
|
||||
}
|
||||
|
||||
// zipBytes returns a zip archive holding the given (name, contents) entries, in order.
|
||||
func zipBytes(t *testing.T, entries [][2]string) []byte {
|
||||
var buf bytes.Buffer
|
||||
w := zip.NewWriter(&buf)
|
||||
for _, e := range entries {
|
||||
f, err := w.Create(e[0])
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write([]byte(e[1]))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, w.Close())
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user