mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
add java cataloger
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package file
|
||||
|
||||
// Source: https://research.swtch.com/glob.go
|
||||
func GlobMatch(pattern, name string) bool {
|
||||
px := 0
|
||||
nx := 0
|
||||
nextPx := 0
|
||||
nextNx := 0
|
||||
for px < len(pattern) || nx < len(name) {
|
||||
if px < len(pattern) {
|
||||
c := pattern[px]
|
||||
switch c {
|
||||
default: // ordinary character
|
||||
if nx < len(name) && name[nx] == c {
|
||||
px++
|
||||
nx++
|
||||
continue
|
||||
}
|
||||
case '?': // single-character wildcard
|
||||
if nx < len(name) {
|
||||
px++
|
||||
nx++
|
||||
continue
|
||||
}
|
||||
case '*': // zero-or-more-character wildcard
|
||||
// Try to match at nx.
|
||||
// If that doesn't work out,
|
||||
// restart at nx+1 next.
|
||||
nextPx = px
|
||||
nextNx = nx + 1
|
||||
px++
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Mismatch. Maybe restart.
|
||||
if 0 < nextNx && nextNx <= len(name) {
|
||||
px = nextPx
|
||||
nx = nextNx
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Matched all of pattern to all of name. Success.
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGlobMatch(t *testing.T) {
|
||||
var tests = []struct {
|
||||
pattern string
|
||||
data string
|
||||
ok bool
|
||||
}{
|
||||
{"", "", true},
|
||||
{"x", "", false},
|
||||
{"", "x", false},
|
||||
{"abc", "abc", true},
|
||||
{"*", "abc", true},
|
||||
{"*c", "abc", true},
|
||||
{"*b", "abc", false},
|
||||
{"a*", "abc", true},
|
||||
{"b*", "abc", false},
|
||||
{"a*", "a", true},
|
||||
{"*a", "a", true},
|
||||
{"a*b*c*d*e*", "axbxcxdxe", true},
|
||||
{"a*b*c*d*e*", "axbxcxdxexxx", true},
|
||||
{"a*b?c*x", "abxbbxdbxebxczzx", true},
|
||||
{"a*b?c*x", "abxbbxdbxebxczzy", false},
|
||||
{"a*a*a*a*b", strings.Repeat("a", 100), false},
|
||||
{"*x", "xxx", true},
|
||||
{"/home/place/**", "/home/place/a/thing", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if GlobMatch(test.pattern, test.data) != test.ok {
|
||||
t.Errorf("failed glob='%s' data='%s'", test.pattern, test.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eux
|
||||
|
||||
zip -r "$1" zip-source
|
||||
@@ -0,0 +1 @@
|
||||
B file...
|
||||
@@ -0,0 +1 @@
|
||||
A file! nice!
|
||||
@@ -0,0 +1,190 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/imgbom/internal/log"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
KB = 1 << (10 * iota)
|
||||
MB
|
||||
GB
|
||||
)
|
||||
|
||||
const readLimit = 2 * GB
|
||||
|
||||
type extractRequest map[string]struct{}
|
||||
|
||||
func newExtractRequest(paths ...string) extractRequest {
|
||||
results := make(extractRequest)
|
||||
for _, p := range paths {
|
||||
results[p] = struct{}{}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func ExtractFilesFromZip(archivePath string, paths ...string) (map[string]string, error) {
|
||||
request := newExtractRequest(paths...)
|
||||
|
||||
results := make(map[string]string)
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
if _, ok := request[file.Name]; !ok {
|
||||
// this file path is not of interest
|
||||
continue
|
||||
}
|
||||
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
return nil, fmt.Errorf("unable to extract directories, only files: %s", file.Name)
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
// limit the zip reader on each file read to prevent decompression bomb attacks
|
||||
numBytes, err := io.Copy(&buffer, io.LimitReader(zippedFile, readLimit))
|
||||
if numBytes >= readLimit || errors.Is(err, io.EOF) {
|
||||
return nil, fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
|
||||
results[file.Name] = buffer.String()
|
||||
|
||||
err = zippedFile.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func UnzipToDir(archivePath, targetDir string) error {
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
// the zip-slip attack protection is still being erroneously detected
|
||||
// nolint:gosec
|
||||
expandedFilePath := filepath.Clean(filepath.Join(targetDir, file.Name))
|
||||
|
||||
// protect against zip slip attacks (traversing unintended parent paths from maliciously crafted relative-path entries)
|
||||
if !strings.HasPrefix(expandedFilePath, filepath.Clean(targetDir)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("potential zip slip attack: %q", expandedFilePath)
|
||||
}
|
||||
|
||||
zippedFile, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
err = os.MkdirAll(expandedFilePath, file.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create dir=%q from zip=%q: %w", expandedFilePath, archivePath, err)
|
||||
}
|
||||
} else {
|
||||
// Open an output file for writing
|
||||
outputFile, err := os.OpenFile(
|
||||
expandedFilePath,
|
||||
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
|
||||
file.Mode(),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
|
||||
}
|
||||
|
||||
// limit the zip reader on each file read to prevent decompression bomb attacks
|
||||
numBytes, err := io.Copy(outputFile, io.LimitReader(zippedFile, readLimit))
|
||||
if numBytes >= readLimit || errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
|
||||
}
|
||||
|
||||
err = outputFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close dest file=%q from zip=%q: %w", outputFile.Name(), archivePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = zippedFile.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ZipManifest map[string]os.FileInfo
|
||||
|
||||
func newZipManifest() ZipManifest {
|
||||
return make(ZipManifest)
|
||||
}
|
||||
|
||||
func (z ZipManifest) Add(entry string, info os.FileInfo) {
|
||||
z[entry] = info
|
||||
}
|
||||
|
||||
func (z ZipManifest) GlobMatch(pattern string) []string {
|
||||
results := make([]string, 0)
|
||||
for entry := range z {
|
||||
if GlobMatch(pattern, entry) {
|
||||
results = append(results, entry)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func ZipFileManifest(archivePath string) (ZipManifest, error) {
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
manifest := newZipManifest()
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"github.com/go-test/deep"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func generateFixture(t *testing.T, archivePath string) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("./generate-zip-fixture.sh", archivePath)
|
||||
cmd.Dir = filepath.Join(cwd, "test-fixtures")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("unable to start generate zip fixture script: %+v", err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// The program has exited with an exit code != 0
|
||||
|
||||
// This works on both Unix and Windows. Although package
|
||||
// syscall is generally platform dependent, WaitStatus is
|
||||
// defined for both Unix and Windows and in both cases has
|
||||
// an ExitStatus() method with the same signature.
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
if status.ExitStatus() != 0 {
|
||||
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("unable to get generate fixture script result: %+v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equal(r1, r2 io.Reader) (bool, error) {
|
||||
w1 := sha256.New()
|
||||
w2 := sha256.New()
|
||||
n1, err1 := io.Copy(w1, r1)
|
||||
if err1 != nil {
|
||||
return false, err1
|
||||
}
|
||||
n2, err2 := io.Copy(w2, r2)
|
||||
if err2 != nil {
|
||||
return false, err2
|
||||
}
|
||||
|
||||
var b1, b2 [sha256.Size]byte
|
||||
copy(b1[:], w1.Sum(nil))
|
||||
copy(b2[:], w2.Sum(nil))
|
||||
|
||||
return n1 != n2 || b1 == b2, nil
|
||||
}
|
||||
|
||||
func TestUnzipToDir(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "imgbom-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
generateFixture(t, archivePrefix.Name())
|
||||
|
||||
contentsDir, err := ioutil.TempDir("", "imgbom-ziputil-contents-TEST-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempdir: %+v", err)
|
||||
}
|
||||
defer os.RemoveAll(contentsDir)
|
||||
|
||||
t.Logf("content path: %s", contentsDir)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
// note: zip utility already includes "zip-source" as a parent dir for all contained files
|
||||
goldenRootDir := filepath.Join(cwd, "test-fixtures")
|
||||
expectedPaths := 4
|
||||
observedPaths := 0
|
||||
|
||||
err = UnzipToDir(archivePath, contentsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
// compare the source dir tree and the unzipped tree
|
||||
err = filepath.Walk(filepath.Join(contentsDir, "zip-source"),
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
t.Logf("unzipped path: %s", path)
|
||||
observedPaths++
|
||||
if err != nil {
|
||||
t.Fatalf("this should not happen")
|
||||
return err
|
||||
}
|
||||
|
||||
goldenPath := filepath.Join(goldenRootDir, strings.TrimPrefix(path, contentsDir))
|
||||
|
||||
if info.IsDir() {
|
||||
i, err := os.Stat(goldenPath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to stat golden path: %+v", err)
|
||||
}
|
||||
if !i.IsDir() {
|
||||
t.Fatalf("mismatched file types: %s", goldenPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// this is a file, not a dir...
|
||||
|
||||
testFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to open test file=%s :%+v", path, err)
|
||||
}
|
||||
|
||||
goldenFile, err := os.Open(goldenPath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to open golden file=%s :%+v", goldenPath, err)
|
||||
}
|
||||
|
||||
same, err := equal(testFile, goldenFile)
|
||||
if err != nil {
|
||||
t.Fatalf("could not compare files (%s, %s): %+v", goldenPath, path, err)
|
||||
}
|
||||
|
||||
if !same {
|
||||
t.Errorf("paths are not the same (%s, %s)", goldenPath, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to walk dir: %+v", err)
|
||||
}
|
||||
|
||||
if observedPaths != expectedPaths {
|
||||
t.Errorf("missed test paths: %d!=%d", observedPaths, expectedPaths)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtractFilesFromZipFile(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "imgbom-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
generateFixture(t, archivePrefix.Name())
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
aFilePath := filepath.Join("zip-source", "some-dir", "a-file.txt")
|
||||
bFilePath := filepath.Join("zip-source", "b-file.txt")
|
||||
|
||||
expected := map[string]string{
|
||||
aFilePath: "A file! nice!",
|
||||
bFilePath: "B file...",
|
||||
}
|
||||
|
||||
actual, err := ExtractFilesFromZip(archivePath, aFilePath, bFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
diffs := deep.Equal(actual, expected)
|
||||
if len(diffs) > 0 {
|
||||
for _, d := range diffs {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(actual, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("can't show results: %+v", err)
|
||||
}
|
||||
|
||||
t.Errorf("full result: %s", string(b))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestZipFileManifest(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "imgbom-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
generateFixture(t, archivePrefix.Name())
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
expected := []string{
|
||||
filepath.Join("zip-source") + string(os.PathSeparator),
|
||||
filepath.Join("zip-source", "some-dir") + string(os.PathSeparator),
|
||||
filepath.Join("zip-source", "some-dir", "a-file.txt"),
|
||||
filepath.Join("zip-source", "b-file.txt"),
|
||||
}
|
||||
|
||||
actual, err := ZipFileManifest(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
if len(expected) != len(actual) {
|
||||
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expected))
|
||||
}
|
||||
|
||||
for _, e := range expected {
|
||||
_, ok := actual[e]
|
||||
if !ok {
|
||||
t.Errorf("missing path: %s", e)
|
||||
}
|
||||
}
|
||||
|
||||
if t.Failed() {
|
||||
|
||||
b, err := json.MarshalIndent(actual, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("can't show results: %+v", err)
|
||||
}
|
||||
|
||||
t.Errorf("full result: %s", string(b))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
package internal
|
||||
|
||||
type Set map[string]struct{}
|
||||
type StringSet map[string]struct{}
|
||||
|
||||
func NewStringSet() Set {
|
||||
return make(Set)
|
||||
func NewStringSet() StringSet {
|
||||
return make(StringSet)
|
||||
}
|
||||
|
||||
func NewStringSetFromSlice(start []string) Set {
|
||||
ret := make(Set)
|
||||
func NewStringSetFromSlice(start []string) StringSet {
|
||||
ret := make(StringSet)
|
||||
for _, s := range start {
|
||||
ret.Add(s)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s Set) Add(i string) {
|
||||
func (s StringSet) Add(i string) {
|
||||
s[i] = struct{}{}
|
||||
}
|
||||
|
||||
func (s Set) Remove(i string) {
|
||||
func (s StringSet) Remove(i string) {
|
||||
delete(s, i)
|
||||
}
|
||||
|
||||
func (s Set) Contains(i string) bool {
|
||||
func (s StringSet) Contains(i string) bool {
|
||||
_, ok := s[i]
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func setupScreen(output *os.File) *frame.Frame {
|
||||
return fr
|
||||
}
|
||||
|
||||
// nolint:funlen,gocognit
|
||||
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) int {
|
||||
output := os.Stderr
|
||||
|
||||
|
||||
Reference in New Issue
Block a user