mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 07:26:34 +01:00
259 lines
9.9 KiB
Go
259 lines
9.9 KiB
Go
package java
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/anchore/syft/internal/archive"
|
|
"github.com/anchore/syft/internal/tmpdir"
|
|
"github.com/anchore/syft/internal/unknown"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
)
|
|
|
|
func Test_archiveContentsCataloger_agreesWithTheArchiveReadingCataloger(t *testing.T) {
|
|
// for the same archive, the contents-reading cataloger must report the same packages with the same
|
|
// identity as the one reading the archive file; any divergence shows up as an SBOM diff
|
|
entries := jarEntries()
|
|
cfg := DefaultArchiveCatalogerConfig()
|
|
|
|
// the archive-reading cataloger, over a directory holding the jar
|
|
archiveDir := t.TempDir()
|
|
jarPath := writeJar(t, archiveDir, "example-lib-1.2.3.jar", entries)
|
|
archiveResolver, err := fileresolver.NewFromDirectory(archiveDir, "")
|
|
require.NoError(t, err)
|
|
// the archive-reading cataloger copies the archive to a temp dir first, which the contents-reading
|
|
// one does not
|
|
archiveCtx := tmpdir.WithValue(context.Background(), tmpdir.FromPath(t.TempDir()))
|
|
fromArchive, _, err := NewArchiveCataloger(cfg).Catalog(archiveCtx, archiveResolver)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, fromArchive)
|
|
|
|
// the contents-reading cataloger, over the same entries laid out as a filesystem, with the traversal
|
|
// the archive cataloger task would have placed on the context
|
|
contentsDir := t.TempDir()
|
|
explode(t, contentsDir, entries)
|
|
contentsResolver, err := fileresolver.NewFromDirectory(contentsDir, "")
|
|
require.NoError(t, err)
|
|
|
|
// the traversal names the archive exactly as the resolver did for the archive-reading cataloger, so
|
|
// both sides derive identity from the same string and the comparison is about where the entries came
|
|
// from rather than how the test spelled a path
|
|
jarLocations, err := archiveResolver.FilesByGlob("**/*.jar")
|
|
require.NoError(t, err)
|
|
require.Len(t, jarLocations, 1)
|
|
archivePath := jarLocations[0].Path()
|
|
|
|
digests := digestsOfFile(t, jarPath)
|
|
trav := &archive.Traversal{
|
|
Location: file.NewLocation(archivePath),
|
|
VirtualPath: archivePath,
|
|
Digests: digests,
|
|
}
|
|
ctx := archive.WithTraversal(context.Background(), trav)
|
|
fromContents, _, err := NewArchiveCataloger(cfg).Catalog(ctx, contentsResolver)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, summarize(fromArchive), summarize(fromContents))
|
|
}
|
|
|
|
func Test_archiveContentsCataloger_reportsNothingOutsideAnArchive(t *testing.T) {
|
|
// with the archive cataloger task owning extraction, every archive comes back with a traversal
|
|
// naming it. Outside one there is nothing to describe: opening archive files here as well would
|
|
// catalog each of them a second time.
|
|
cfg := DefaultArchiveCatalogerConfig()
|
|
cfg.NestedArchivesHandledExternally = true
|
|
|
|
dir := t.TempDir()
|
|
jarPath := writeJar(t, dir, "example-lib-1.2.3.jar", jarEntries())
|
|
require.FileExists(t, jarPath)
|
|
resolver, err := fileresolver.NewFromDirectory(dir, "")
|
|
require.NoError(t, err)
|
|
|
|
pkgs, _, err := NewArchiveCataloger(cfg).Catalog(context.Background(), resolver)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, pkgs)
|
|
}
|
|
|
|
func Test_archiveContentsCataloger_reportsNothingForAnArchiveThatIsNotJava(t *testing.T) {
|
|
// a .zip is an archive and its contents are not a jar's; the archive-reading cataloger draws that
|
|
// line with its globs and this one draws it with the same list
|
|
dir := t.TempDir()
|
|
explode(t, dir, jarEntries())
|
|
resolver, err := fileresolver.NewFromDirectory(dir, "")
|
|
require.NoError(t, err)
|
|
|
|
ctx := archive.WithTraversal(context.Background(), &archive.Traversal{
|
|
Location: file.NewLocation("/bundle.zip"),
|
|
VirtualPath: "/bundle.zip",
|
|
})
|
|
pkgs, _, err := NewArchiveCataloger(DefaultArchiveCatalogerConfig()).Catalog(ctx, resolver)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, pkgs)
|
|
}
|
|
|
|
func Test_archiveContentsCataloger_noManifestIsNoPackage(t *testing.T) {
|
|
// unchanged from the archive-reading cataloger, and the whole coverage argument: a jar with no
|
|
// META-INF/MANIFEST.MF yields nothing today either, so requiring the contents to be traversed takes
|
|
// away less than it appears to. The maven metadata here is never consulted - without a main package
|
|
// there is nothing to attach it to.
|
|
entries := map[string]string{
|
|
"META-INF/maven/com.other/bundled-dep/pom.properties": "groupId=com.other\nartifactId=bundled-dep\nversion=4.5.6\n",
|
|
"com/example/Thing.class": "not really bytecode",
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
explode(t, dir, entries)
|
|
resolver, err := fileresolver.NewFromDirectory(dir, "")
|
|
require.NoError(t, err)
|
|
|
|
ctx := archive.WithTraversal(context.Background(), &archive.Traversal{
|
|
Location: file.NewLocation("no-manifest-1.0.0.jar"),
|
|
VirtualPath: "no-manifest-1.0.0.jar",
|
|
})
|
|
pkgs, _, err := NewArchiveCataloger(DefaultArchiveCatalogerConfig()).Catalog(ctx, resolver)
|
|
assert.Empty(t, pkgs)
|
|
// reported against the archive rather than swallowed, and as an unknown rather than a scan failure
|
|
require.Error(t, err)
|
|
coordErrs, remaining := unknown.ExtractCoordinateErrors(err)
|
|
assert.NoError(t, remaining)
|
|
require.Len(t, coordErrs, 1)
|
|
assert.Equal(t, "no-manifest-1.0.0.jar", coordErrs[0].Coordinates.RealPath)
|
|
}
|
|
|
|
func Test_archiveContentsCataloger_readsWhatWasExtractedAndNoMore(t *testing.T) {
|
|
// the sharp edge of reading contents rather than the archive: this cataloger sees exactly what the
|
|
// extraction produced. An archive truncated by a limit yields a package built from the entries that
|
|
// were written, so identity degrades rather than disappearing - the manifest is there, the maven
|
|
// metadata is not.
|
|
//
|
|
// Observed on a real artifact: jenkins-core-2.578.jar holds more than the default 10,000-entry bound,
|
|
// so its META-INF/maven pom is never extracted and its group id falls back from org.jenkins-ci.main
|
|
// to the artifact id. The archive-reading cataloger never felt this, reading the archive's own
|
|
// central directory with no bound on it.
|
|
entries := map[string]string{
|
|
"META-INF/MANIFEST.MF": "Manifest-Version: 1.0\n" +
|
|
"Implementation-Title: example-lib\nImplementation-Version: 1.2.3\n",
|
|
// the pom that would have supplied the group id is missing, as truncation would leave it
|
|
"com/example/Thing.class": "not really bytecode",
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
explode(t, dir, entries)
|
|
resolver, err := fileresolver.NewFromDirectory(dir, "")
|
|
require.NoError(t, err)
|
|
|
|
ctx := archive.WithTraversal(context.Background(), &archive.Traversal{
|
|
Location: file.NewLocation("example-lib-1.2.3.jar"),
|
|
VirtualPath: "example-lib-1.2.3.jar",
|
|
})
|
|
pkgs, _, err := NewArchiveCataloger(DefaultArchiveCatalogerConfig()).Catalog(ctx, resolver)
|
|
require.NoError(t, err)
|
|
require.Len(t, pkgs, 1)
|
|
assert.Equal(t, "example-lib", pkgs[0].Name)
|
|
assert.Equal(t, "1.2.3", pkgs[0].Version)
|
|
assert.Equal(t, "pkg:maven/example-lib/example-lib@1.2.3", pkgs[0].PURL,
|
|
"with no pom to name the group, the group falls back to the artifact id")
|
|
}
|
|
|
|
// jarEntries is one java archive's worth of content, used both to build the archive the
|
|
// archive-reading cataloger parses and to lay out the filesystem the contents-reading one reads. The
|
|
// tests below assert the two agree.
|
|
func jarEntries() map[string]string {
|
|
return map[string]string{
|
|
"META-INF/MANIFEST.MF": "Manifest-Version: 1.0\n" +
|
|
"Implementation-Title: example-lib\n" +
|
|
"Implementation-Version: 1.2.3\n",
|
|
"META-INF/maven/com.example/example-lib/pom.properties": "groupId=com.example\n" +
|
|
"artifactId=example-lib\nversion=1.2.3\n",
|
|
"META-INF/maven/com.example/example-lib/pom.xml": `<project><modelVersion>4.0.0</modelVersion>
|
|
<groupId>com.example</groupId><artifactId>example-lib</artifactId><version>1.2.3</version>
|
|
<licenses><license><name>Apache License 2.0</name><url>https://www.apache.org/licenses/LICENSE-2.0</url></license></licenses>
|
|
</project>`,
|
|
// a bundled dependency's maven metadata, which becomes a package of its own
|
|
"META-INF/maven/com.other/bundled-dep/pom.properties": "groupId=com.other\n" +
|
|
"artifactId=bundled-dep\nversion=4.5.6\n",
|
|
"com/example/Thing.class": "not really bytecode",
|
|
}
|
|
}
|
|
|
|
func writeJar(t *testing.T, dir, name string, entries map[string]string) string {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
names := make([]string, 0, len(entries))
|
|
for n := range entries {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for _, n := range names {
|
|
w, err := zw.Create(n)
|
|
require.NoError(t, err)
|
|
_, err = w.Write([]byte(entries[n]))
|
|
require.NoError(t, err)
|
|
}
|
|
require.NoError(t, zw.Close())
|
|
|
|
p := filepath.Join(dir, name)
|
|
require.NoError(t, os.WriteFile(p, buf.Bytes(), 0o600))
|
|
return p
|
|
}
|
|
|
|
// explode lays the same entries out as files, what the archive cataloger task hands the sub-pipeline
|
|
// once it has extracted an archive.
|
|
func explode(t *testing.T, dir string, entries map[string]string) {
|
|
t.Helper()
|
|
for name, contents := range entries {
|
|
p := filepath.Join(dir, filepath.FromSlash(name))
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755))
|
|
require.NoError(t, os.WriteFile(p, []byte(contents), 0o600))
|
|
}
|
|
}
|
|
|
|
func summarize(pkgs []pkg.Package) []string {
|
|
var out []string
|
|
for _, p := range pkgs {
|
|
meta, _ := p.Metadata.(pkg.JavaArchive)
|
|
lics := p.Licenses.ToSlice()
|
|
names := make([]string, 0, len(lics))
|
|
for _, l := range lics {
|
|
names = append(names, l.Value)
|
|
}
|
|
sort.Strings(names)
|
|
out = append(out, p.Name+"@"+p.Version+" purl="+p.PURL+" vpath="+meta.VirtualPath+" lics="+
|
|
joinSorted(names))
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func joinSorted(in []string) string {
|
|
out := ""
|
|
for i, s := range in {
|
|
if i > 0 {
|
|
out += ","
|
|
}
|
|
out += s
|
|
}
|
|
return out
|
|
}
|
|
|
|
func digestsOfFile(t *testing.T, path string) []file.Digest {
|
|
t.Helper()
|
|
entries, err := newZipEntries(context.Background(), path)
|
|
require.NoError(t, err)
|
|
digests, err := entries.digests(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, digests)
|
|
return digests
|
|
}
|