mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 15:06:36 +01:00
feat: Support scanning license files in golang packages over the network (#1630)
Signed-off-by: Avi Deitcher <avi@deitcher.net> Signed-off-by: Keith Zantow <kzantow@gmail.com> Signed-off-by: Alex Goodman <alex.goodman@anchore.com> Co-authored-by: Keith Zantow <kzantow@gmail.com> Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
co-authored by
Keith Zantow
Alex Goodman
parent
44422853be
commit
b69259534d
@@ -0,0 +1,87 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
)
|
||||
|
||||
// billyFSAdapter is a fs.FS, fs.ReadDirFS, and fs.StatFS wrapping what billyfs returns
|
||||
type billyFSAdapter struct {
|
||||
fs billy.Filesystem
|
||||
}
|
||||
|
||||
func (b billyFSAdapter) Stat(name string) (fs.FileInfo, error) {
|
||||
return b.fs.Stat(name)
|
||||
}
|
||||
|
||||
func (b billyFSAdapter) ReadDir(name string) (out []fs.DirEntry, _ error) {
|
||||
entries, err := b.fs.ReadDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
out = append(out, billyDirEntry{fi: e})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b billyFSAdapter) Open(name string) (fs.File, error) {
|
||||
f, err := b.fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := b.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return billyFile{f: f, fi: fi}, nil
|
||||
}
|
||||
|
||||
var _ fs.FS = (*billyFSAdapter)(nil)
|
||||
var _ fs.ReadDirFS = (*billyFSAdapter)(nil)
|
||||
var _ fs.StatFS = (*billyFSAdapter)(nil)
|
||||
|
||||
// billyFile is a fs.File wrapping what billyfs returns
|
||||
type billyFile struct {
|
||||
f billy.File
|
||||
fi fs.FileInfo
|
||||
}
|
||||
|
||||
func (b billyFile) Stat() (fs.FileInfo, error) {
|
||||
return b.fi, nil
|
||||
}
|
||||
|
||||
func (b billyFile) Read(i []byte) (int, error) {
|
||||
return b.f.Read(i)
|
||||
}
|
||||
|
||||
func (b billyFile) Close() error {
|
||||
return b.f.Close()
|
||||
}
|
||||
|
||||
var _ fs.File = (*billyFile)(nil)
|
||||
|
||||
// billyDirEntry is a fs.DirEntry wrapping what billyfs returns
|
||||
type billyDirEntry struct {
|
||||
fi os.FileInfo
|
||||
}
|
||||
|
||||
func (b billyDirEntry) Name() string {
|
||||
return b.fi.Name()
|
||||
}
|
||||
|
||||
func (b billyDirEntry) IsDir() bool {
|
||||
return b.fi.IsDir()
|
||||
}
|
||||
|
||||
func (b billyDirEntry) Type() fs.FileMode {
|
||||
return b.fi.Mode()
|
||||
}
|
||||
|
||||
func (b billyDirEntry) Info() (fs.FileInfo, error) {
|
||||
return b.fi, nil
|
||||
}
|
||||
|
||||
var _ fs.DirEntry = (*billyDirEntry)(nil)
|
||||
@@ -0,0 +1,33 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_billyFSAdapter(t *testing.T) {
|
||||
r, err := git.PlainInit("test-fixtures/repo", false)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = os.RemoveAll("test-fixtures/repo/.git")
|
||||
})
|
||||
|
||||
wt, err := r.Worktree()
|
||||
require.NoError(t, err)
|
||||
f := billyFSAdapter{
|
||||
fs: wt.Filesystem,
|
||||
}
|
||||
|
||||
found := ""
|
||||
err = fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
found = path
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "LICENSE", found)
|
||||
}
|
||||
@@ -5,28 +5,47 @@ package golang
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type GoCatalogerOpts struct {
|
||||
SearchLocalModCacheLicenses bool
|
||||
LocalModCacheDir string
|
||||
}
|
||||
|
||||
// NewGoModFileCataloger returns a new Go module cataloger object.
|
||||
func NewGoModFileCataloger(opts GoCatalogerOpts) *generic.Cataloger {
|
||||
func NewGoModFileCataloger(opts GoCatalogerOpts) pkg.Cataloger {
|
||||
c := goModCataloger{
|
||||
licenses: newGoLicenses(opts),
|
||||
}
|
||||
return generic.NewCataloger("go-mod-file-cataloger").
|
||||
WithParserByGlobs(c.parseGoModFile, "**/go.mod")
|
||||
return &progressingCataloger{
|
||||
progress: c.licenses.progress,
|
||||
cataloger: generic.NewCataloger("go-mod-file-cataloger").
|
||||
WithParserByGlobs(c.parseGoModFile, "**/go.mod"),
|
||||
}
|
||||
}
|
||||
|
||||
// NewGoModuleBinaryCataloger returns a new Golang cataloger object.
|
||||
func NewGoModuleBinaryCataloger(opts GoCatalogerOpts) *generic.Cataloger {
|
||||
func NewGoModuleBinaryCataloger(opts GoCatalogerOpts) pkg.Cataloger {
|
||||
c := goBinaryCataloger{
|
||||
licenses: newGoLicenses(opts),
|
||||
}
|
||||
return generic.NewCataloger("go-module-binary-cataloger").
|
||||
WithParserByMimeTypes(c.parseGoBinary, internal.ExecutableMIMETypeSet.List()...)
|
||||
return &progressingCataloger{
|
||||
progress: c.licenses.progress,
|
||||
cataloger: generic.NewCataloger("go-module-binary-cataloger").
|
||||
WithParserByMimeTypes(c.parseGoBinary, internal.ExecutableMIMETypeSet.List()...),
|
||||
}
|
||||
}
|
||||
|
||||
type progressingCataloger struct {
|
||||
progress *event.CatalogerTask
|
||||
cataloger *generic.Cataloger
|
||||
}
|
||||
|
||||
func (p *progressingCataloger) Name() string {
|
||||
return p.cataloger.Name()
|
||||
}
|
||||
|
||||
func (p *progressingCataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
defer p.progress.SetCompleted()
|
||||
return p.cataloger.Catalog(resolver)
|
||||
}
|
||||
|
||||
@@ -1,105 +1,158 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type goLicenses struct {
|
||||
searchLocalModCacheLicenses bool
|
||||
localModCacheResolver source.FileResolver
|
||||
opts GoCatalogerOpts
|
||||
localModCacheResolver source.WritableFileResolver
|
||||
progress *event.CatalogerTask
|
||||
}
|
||||
|
||||
func newGoLicenses(opts GoCatalogerOpts) goLicenses {
|
||||
return goLicenses{
|
||||
searchLocalModCacheLicenses: opts.SearchLocalModCacheLicenses,
|
||||
localModCacheResolver: modCacheResolver(opts.LocalModCacheDir),
|
||||
opts: opts,
|
||||
localModCacheResolver: modCacheResolver(opts.localModCacheDir),
|
||||
progress: &event.CatalogerTask{
|
||||
SubStatus: true,
|
||||
RemoveOnCompletion: true,
|
||||
Title: "Downloading go mod",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultGoPath() string {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
|
||||
if goPath == "" {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Debug("unable to determine user home dir: %v", err)
|
||||
} else {
|
||||
goPath = path.Join(homeDir, "go")
|
||||
func remotesForModule(proxies []string, noProxy []string, module string) []string {
|
||||
for _, pattern := range noProxy {
|
||||
if matched, err := path.Match(pattern, module); err == nil && matched {
|
||||
// matched to be direct for this module
|
||||
return directProxiesOnly
|
||||
}
|
||||
}
|
||||
|
||||
return goPath
|
||||
return proxies
|
||||
}
|
||||
|
||||
// resolver needs to be shared between mod file & binary scanners so it's only scanned once
|
||||
var modCacheResolvers = map[string]source.FileResolver{}
|
||||
|
||||
func modCacheResolver(modCacheDir string) source.FileResolver {
|
||||
if modCacheDir == "" {
|
||||
goPath := defaultGoPath()
|
||||
if goPath != "" {
|
||||
modCacheDir = path.Join(goPath, "pkg", "mod")
|
||||
}
|
||||
}
|
||||
|
||||
if r, ok := modCacheResolvers[modCacheDir]; ok {
|
||||
return r
|
||||
}
|
||||
|
||||
var r source.FileResolver
|
||||
func modCacheResolver(modCacheDir string) source.WritableFileResolver {
|
||||
var r source.WritableFileResolver
|
||||
|
||||
if modCacheDir == "" {
|
||||
log.Trace("unable to determine mod cache directory, skipping mod cache resolver")
|
||||
r = source.NewMockResolverForPaths()
|
||||
r = source.EmptyResolver{}
|
||||
} else {
|
||||
stat, err := os.Stat(modCacheDir)
|
||||
|
||||
if os.IsNotExist(err) || stat == nil || !stat.IsDir() {
|
||||
log.Tracef("unable to open mod cache directory: %s, skipping mod cache resolver", modCacheDir)
|
||||
r = source.NewMockResolverForPaths()
|
||||
r = source.EmptyResolver{}
|
||||
} else {
|
||||
r = source.NewDeferredResolverFromSource(func() (source.Source, error) {
|
||||
return source.NewFromDirectory(modCacheDir)
|
||||
})
|
||||
r = source.NewUnindexedDirectoryResolver(modCacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
modCacheResolvers[modCacheDir] = r
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicenses(resolver source.FileResolver, moduleName, moduleVersion string) (licenses []string, err error) {
|
||||
moduleName = processCaps(moduleName)
|
||||
|
||||
licenses, err = findLicenses(resolver,
|
||||
fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, moduleName, moduleVersion),
|
||||
fmt.Sprintf(`**/go/pkg/mod/%s@%s/*`, processCaps(moduleName), moduleVersion),
|
||||
)
|
||||
|
||||
if c.searchLocalModCacheLicenses && err == nil && len(licenses) == 0 {
|
||||
// if we're running against a directory on the filesystem, it may not include the
|
||||
// user's homedir / GOPATH, so we defer to using the localModCacheResolver
|
||||
licenses, err = findLicenses(c.localModCacheResolver,
|
||||
fmt.Sprintf(`**/%s@%s/*`, moduleName, moduleVersion),
|
||||
)
|
||||
if err != nil || len(licenses) > 0 {
|
||||
return requireCollection(licenses), err
|
||||
}
|
||||
|
||||
// always return a non-nil slice
|
||||
// look in the local host mod cache...
|
||||
licenses, err = c.getLicensesFromLocal(moduleName, moduleVersion)
|
||||
if err != nil || len(licenses) > 0 {
|
||||
return requireCollection(licenses), err
|
||||
}
|
||||
|
||||
// we did not find it yet and remote searching was enabled
|
||||
licenses, err = c.getLicensesFromRemote(moduleName, moduleVersion)
|
||||
return requireCollection(licenses), err
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicensesFromLocal(moduleName, moduleVersion string) ([]string, error) {
|
||||
if !c.opts.searchLocalModCacheLicenses {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if we're running against a directory on the filesystem, it may not include the
|
||||
// user's homedir / GOPATH, so we defer to using the localModCacheResolver
|
||||
return findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion))
|
||||
}
|
||||
|
||||
func (c *goLicenses) getLicensesFromRemote(moduleName, moduleVersion string) ([]string, error) {
|
||||
if !c.opts.searchRemoteLicenses {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proxies := remotesForModule(c.opts.proxies, c.opts.noProxy, moduleName)
|
||||
|
||||
fsys, err := getModule(c.progress, proxies, moduleName, moduleVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dir := moduleDir(moduleName, moduleVersion)
|
||||
|
||||
// populate the mod cache with the results
|
||||
err = fs.WalkDir(fsys, ".", func(filePath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
f, err := fsys.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.localModCacheResolver.Write(source.NewLocation(path.Join(dir, filePath)), f)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Tracef("remote proxy walk failed for: %s", moduleName)
|
||||
}
|
||||
|
||||
return findLicenses(c.localModCacheResolver, moduleSearchGlob(moduleName, moduleVersion))
|
||||
}
|
||||
|
||||
func moduleDir(moduleName, moduleVersion string) string {
|
||||
return fmt.Sprintf("%s@%s", processCaps(moduleName), moduleVersion)
|
||||
}
|
||||
|
||||
func moduleSearchGlob(moduleName, moduleVersion string) string {
|
||||
return fmt.Sprintf("%s/*", moduleDir(moduleName, moduleVersion))
|
||||
}
|
||||
|
||||
func requireCollection(licenses []string) []string {
|
||||
if licenses == nil {
|
||||
licenses = []string{}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return
|
||||
return licenses
|
||||
}
|
||||
|
||||
func findLicenses(resolver source.FileResolver, globMatch string) (out []string, err error) {
|
||||
@@ -138,3 +191,96 @@ func processCaps(s string) string {
|
||||
return "!" + strings.ToLower(s)
|
||||
})
|
||||
}
|
||||
|
||||
func getModule(progress *event.CatalogerTask, proxies []string, moduleName, moduleVersion string) (fsys fs.FS, err error) {
|
||||
for _, proxy := range proxies {
|
||||
u, _ := url.Parse(proxy)
|
||||
if proxy == "direct" {
|
||||
fsys, err = getModuleRepository(progress, moduleName, moduleVersion)
|
||||
continue
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "https", "http":
|
||||
fsys, err = getModuleProxy(progress, proxy, moduleName, moduleVersion)
|
||||
case "file":
|
||||
p := filepath.Join(u.Path, moduleName, "@v", moduleVersion)
|
||||
progress.SetValue(fmt.Sprintf("file: %s", p))
|
||||
fsys = os.DirFS(p)
|
||||
}
|
||||
if fsys != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getModuleProxy(progress *event.CatalogerTask, proxy string, moduleName string, moduleVersion string) (out fs.FS, _ error) {
|
||||
u := fmt.Sprintf("%s/%s/@v/%s.zip", proxy, moduleName, moduleVersion)
|
||||
progress.SetValue(u)
|
||||
// get the module zip
|
||||
resp, err := http.Get(u) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
u = fmt.Sprintf("%s/%s/@v/%s.zip", proxy, strings.ToLower(moduleName), moduleVersion)
|
||||
progress.SetValue(u)
|
||||
// try lowercasing it; some packages have mixed casing that really messes up the proxy
|
||||
resp, err = http.Get(u) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get module zip: %s", resp.Status)
|
||||
}
|
||||
}
|
||||
// read the zip
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err = zip.NewReader(bytes.NewReader(b), resp.ContentLength)
|
||||
versionPath := findVersionPath(out, ".")
|
||||
out = getSubFS(out, versionPath)
|
||||
return out, err
|
||||
}
|
||||
|
||||
func findVersionPath(f fs.FS, dir string) string {
|
||||
list, _ := fs.ReadDir(f, dir)
|
||||
for _, entry := range list {
|
||||
name := entry.Name()
|
||||
if strings.Contains(name, "@") {
|
||||
return name
|
||||
}
|
||||
found := findVersionPath(f, path.Join(dir, name))
|
||||
if found != "" {
|
||||
return path.Join(name, found)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getModuleRepository(progress *event.CatalogerTask, moduleName string, moduleVersion string) (fs.FS, error) {
|
||||
repoName := moduleName
|
||||
parts := strings.Split(moduleName, "/")
|
||||
if len(parts) > 2 {
|
||||
repoName = fmt.Sprintf("%s/%s/%s", parts[0], parts[1], parts[2])
|
||||
}
|
||||
progress.SetValue(fmt.Sprintf("git: %s", repoName))
|
||||
f := memfs.New()
|
||||
buf := &bytes.Buffer{}
|
||||
_, err := git.Clone(memory.NewStorage(), f, &git.CloneOptions{
|
||||
URL: fmt.Sprintf("https://%s", repoName),
|
||||
ReferenceName: plumbing.NewTagReferenceName(moduleVersion), // FIXME version might be a SHA
|
||||
SingleBranch: true,
|
||||
Depth: 1,
|
||||
Progress: buf,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w -- %s", err, buf.String())
|
||||
}
|
||||
|
||||
return billyFSAdapter{fs: f}, nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -10,7 +16,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Test_LicenseSearch(t *testing.T) {
|
||||
func Test_LocalLicenseSearch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
@@ -34,10 +40,85 @@ func Test_LicenseSearch(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
l := newGoLicenses(GoCatalogerOpts{
|
||||
SearchLocalModCacheLicenses: true,
|
||||
LocalModCacheDir: path.Join(wd, "test-fixtures", "licenses"),
|
||||
searchLocalModCacheLicenses: true,
|
||||
localModCacheDir: path.Join(wd, "test-fixtures", "licenses", "pkg", "mod"),
|
||||
})
|
||||
licenses, err := l.getLicenses(source.MockResolver{}, test.name, test.version)
|
||||
licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, licenses, 1)
|
||||
|
||||
require.Equal(t, test.expected, licenses[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RemoteProxyLicenseSearch(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
buf := &bytes.Buffer{}
|
||||
uri := strings.TrimPrefix(strings.TrimSuffix(r.RequestURI, ".zip"), "/")
|
||||
|
||||
parts := strings.Split(uri, "/@v/")
|
||||
modPath := parts[0]
|
||||
modVersion := parts[1]
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
testDir := path.Join(wd, "test-fixtures", "licenses", "pkg", "mod", processCaps(modPath)+"@"+modVersion)
|
||||
|
||||
archive := zip.NewWriter(buf)
|
||||
|
||||
entries, err := os.ReadDir(testDir)
|
||||
require.NoError(t, err)
|
||||
for _, f := range entries {
|
||||
// the zip files downloaded contain a path to the repo that somewhat matches where it ends up on disk,
|
||||
// so prefix entries with something similar
|
||||
writer, err := archive.Create(path.Join("github.com/something/some@version", f.Name()))
|
||||
require.NoError(t, err)
|
||||
contents, err := os.ReadFile(path.Join(testDir, f.Name()))
|
||||
require.NoError(t, err)
|
||||
_, err = writer.Write(contents)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
err = archive.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
w.Header().Add("Content-Length", fmt.Sprintf("%d", buf.Len()))
|
||||
|
||||
_, err = w.Write(buf.Bytes())
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "github.com/someorg/somename",
|
||||
version: "v0.3.2",
|
||||
expected: "Apache-2.0",
|
||||
},
|
||||
{
|
||||
name: "github.com/CapORG/CapProject",
|
||||
version: "v4.111.5",
|
||||
expected: "MIT",
|
||||
},
|
||||
}
|
||||
|
||||
modDir := path.Join(t.TempDir())
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
l := newGoLicenses(GoCatalogerOpts{
|
||||
searchRemoteLicenses: true,
|
||||
proxies: []string{server.URL},
|
||||
localModCacheDir: modDir,
|
||||
})
|
||||
|
||||
licenses, err := l.getLicenses(source.EmptyResolver{}, test.name, test.version)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, licenses, 1)
|
||||
@@ -74,3 +155,42 @@ func Test_processCaps(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_remotesForModule(t *testing.T) {
|
||||
allProxies := []string{"https://somewhere.org", "direct"}
|
||||
directProxy := []string{"direct"}
|
||||
|
||||
tests := []struct {
|
||||
module string
|
||||
noProxy string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
module: "github.com/anchore/syft",
|
||||
expected: allProxies,
|
||||
},
|
||||
{
|
||||
module: "github.com/anchore/sbom-action",
|
||||
noProxy: "*/anchore/*",
|
||||
expected: directProxy,
|
||||
},
|
||||
{
|
||||
module: "github.com/anchore/sbom-action",
|
||||
noProxy: "*/user/mod,*/anchore/sbom-action",
|
||||
expected: directProxy,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.module, func(t *testing.T) {
|
||||
got := remotesForModule(allProxies, strings.Split(test.noProxy, ","), test.module)
|
||||
require.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_findVersionPath(t *testing.T) {
|
||||
f := os.DirFS("test-fixtures/zip-fs")
|
||||
vp := findVersionPath(f, ".")
|
||||
require.Equal(t, "github.com/someorg/somepkg@version", vp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultProxies = "https://proxy.golang.org,direct"
|
||||
directProxyOnly = "direct"
|
||||
)
|
||||
|
||||
var (
|
||||
directProxiesOnly = []string{directProxyOnly}
|
||||
)
|
||||
|
||||
type GoCatalogerOpts struct {
|
||||
searchLocalModCacheLicenses bool
|
||||
localModCacheDir string
|
||||
searchRemoteLicenses bool
|
||||
proxies []string
|
||||
noProxy []string
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithSearchLocalModCacheLicenses(input bool) GoCatalogerOpts {
|
||||
g.searchLocalModCacheLicenses = input
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithLocalModCacheDir(input string) GoCatalogerOpts {
|
||||
if input == "" {
|
||||
return g
|
||||
}
|
||||
g.localModCacheDir = input
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithSearchRemoteLicenses(input bool) GoCatalogerOpts {
|
||||
g.searchRemoteLicenses = input
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithProxy(input string) GoCatalogerOpts {
|
||||
if input == "" {
|
||||
return g
|
||||
}
|
||||
if input == "off" {
|
||||
input = directProxyOnly
|
||||
}
|
||||
g.proxies = strings.Split(input, ",")
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GoCatalogerOpts) WithNoProxy(input string) GoCatalogerOpts {
|
||||
if input == "" {
|
||||
return g
|
||||
}
|
||||
g.noProxy = strings.Split(input, ",")
|
||||
return g
|
||||
}
|
||||
|
||||
// NewGoCatalogerOpts create a GoCatalogerOpts with default options, which includes:
|
||||
// - setting the default remote proxy if none is provided
|
||||
// - setting the default no proxy if none is provided
|
||||
// - setting the default local module cache dir if none is provided
|
||||
func NewGoCatalogerOpts() GoCatalogerOpts {
|
||||
g := GoCatalogerOpts{}
|
||||
|
||||
// first process the proxy settings
|
||||
if len(g.proxies) == 0 {
|
||||
goProxy := os.Getenv("GOPROXY")
|
||||
if goProxy == "" {
|
||||
goProxy = defaultProxies
|
||||
}
|
||||
g = g.WithProxy(goProxy)
|
||||
}
|
||||
|
||||
// next process the gonoproxy settings
|
||||
if len(g.noProxy) == 0 {
|
||||
goPrivate := os.Getenv("GOPRIVATE")
|
||||
goNoProxy := os.Getenv("GONOPROXY")
|
||||
// we only use the env var if it was not set explicitly
|
||||
if goPrivate != "" {
|
||||
g.noProxy = append(g.noProxy, strings.Split(goPrivate, ",")...)
|
||||
}
|
||||
|
||||
// next process the goprivate settings; we always add those
|
||||
if goNoProxy != "" {
|
||||
g.noProxy = append(g.noProxy, strings.Split(goNoProxy, ",")...)
|
||||
}
|
||||
}
|
||||
|
||||
if g.localModCacheDir == "" {
|
||||
goPath := os.Getenv("GOPATH")
|
||||
|
||||
if goPath == "" {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
log.Debug("unable to determine user home dir: %v", err)
|
||||
} else {
|
||||
goPath = path.Join(homeDir, "go")
|
||||
}
|
||||
}
|
||||
if goPath != "" {
|
||||
g.localModCacheDir = path.Join(goPath, "pkg", "mod")
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Options(t *testing.T) {
|
||||
type opts struct {
|
||||
local bool
|
||||
cacheDir string
|
||||
remote bool
|
||||
proxy string
|
||||
noProxy string
|
||||
}
|
||||
|
||||
homedirCacheDisabled := homedir.DisableCache
|
||||
homedir.DisableCache = true
|
||||
t.Cleanup(func() {
|
||||
homedir.DisableCache = homedirCacheDisabled
|
||||
})
|
||||
|
||||
allEnv := map[string]string{
|
||||
"HOME": "/usr/home",
|
||||
"GOPATH": "",
|
||||
"GOPROXY": "",
|
||||
"GOPRIVATE": "",
|
||||
"GONOPROXY": "",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
opts opts
|
||||
expected GoCatalogerOpts
|
||||
}{
|
||||
{
|
||||
name: "set via env defaults",
|
||||
env: map[string]string{
|
||||
"GOPATH": "/go",
|
||||
"GOPROXY": "https://my.proxy",
|
||||
"GOPRIVATE": "my.private",
|
||||
"GONOPROXY": "no.proxy",
|
||||
},
|
||||
opts: opts{},
|
||||
expected: GoCatalogerOpts{
|
||||
searchLocalModCacheLicenses: false,
|
||||
localModCacheDir: "/go/pkg/mod",
|
||||
searchRemoteLicenses: false,
|
||||
proxies: []string{"https://my.proxy"},
|
||||
noProxy: []string{"my.private", "no.proxy"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set via configuration",
|
||||
env: map[string]string{
|
||||
"GOPATH": "/go",
|
||||
"GOPROXY": "https://my.proxy",
|
||||
"GOPRIVATE": "my.private",
|
||||
"GONOPROXY": "no.proxy",
|
||||
},
|
||||
opts: opts{
|
||||
local: true,
|
||||
cacheDir: "/go-cache",
|
||||
remote: true,
|
||||
proxy: "https://alt.proxy,direct",
|
||||
noProxy: "alt.no.proxy",
|
||||
},
|
||||
expected: GoCatalogerOpts{
|
||||
searchLocalModCacheLicenses: true,
|
||||
localModCacheDir: "/go-cache",
|
||||
searchRemoteLicenses: true,
|
||||
proxies: []string{"https://alt.proxy", "direct"},
|
||||
noProxy: []string{"alt.no.proxy"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for k, v := range allEnv {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
for k, v := range test.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
got := NewGoCatalogerOpts().
|
||||
WithSearchLocalModCacheLicenses(test.opts.local).
|
||||
WithLocalModCacheDir(test.opts.cacheDir).
|
||||
WithSearchRemoteLicenses(test.opts.remote).
|
||||
WithProxy(test.opts.proxy).
|
||||
WithNoProxy(test.opts.noProxy)
|
||||
|
||||
assert.Equal(t, test.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -510,7 +510,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
||||
)
|
||||
|
||||
c := goBinaryCataloger{}
|
||||
pkgs := c.buildGoPkgInfo(source.NewMockResolverForPaths(), location, test.mod, test.arch)
|
||||
pkgs := c.buildGoPkgInfo(source.EmptyResolver{}, location, test.mod, test.arch)
|
||||
assert.Equal(t, test.expected, pkgs)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type subFS struct {
|
||||
root string
|
||||
f fs.FS
|
||||
}
|
||||
|
||||
func getSubFS(f fs.FS, root string) fs.FS {
|
||||
if s, ok := f.(fs.SubFS); ok {
|
||||
s, err := s.Sub(root)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return newSubFS(f, root)
|
||||
}
|
||||
|
||||
func newSubFS(f fs.FS, root string) fs.FS {
|
||||
return subFS{
|
||||
root: root,
|
||||
f: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (s subFS) Open(name string) (fs.File, error) {
|
||||
if name == "." {
|
||||
return rootFile{
|
||||
s: s,
|
||||
}, nil
|
||||
}
|
||||
return s.f.Open(path.Join(s.root, name))
|
||||
}
|
||||
|
||||
type rootFile struct {
|
||||
s subFS
|
||||
}
|
||||
|
||||
func (r rootFile) Name() string {
|
||||
return "."
|
||||
}
|
||||
|
||||
func (r rootFile) Size() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (r rootFile) Mode() fs.FileMode {
|
||||
return fs.ModePerm
|
||||
}
|
||||
|
||||
func (r rootFile) ModTime() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (r rootFile) IsDir() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r rootFile) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r rootFile) ReadDir(_ int) ([]fs.DirEntry, error) {
|
||||
return fs.ReadDir(r.s.f, r.s.root)
|
||||
}
|
||||
|
||||
func (r rootFile) Stat() (fs.FileInfo, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r rootFile) Read(_ []byte) (int, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (r rootFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ fs.File = (*rootFile)(nil)
|
||||
var _ fs.ReadDirFile = (*rootFile)(nil)
|
||||
var _ fs.FileInfo = (*rootFile)(nil)
|
||||
|
||||
type subFsFileInfo struct {
|
||||
fi fs.FileInfo
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Name() string {
|
||||
return s.fi.Name()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Size() int64 {
|
||||
return s.fi.Size()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Mode() fs.FileMode {
|
||||
return s.fi.Mode()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) ModTime() time.Time {
|
||||
return s.fi.ModTime()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) IsDir() bool {
|
||||
return s.fi.IsDir()
|
||||
}
|
||||
|
||||
func (s subFsFileInfo) Sys() any {
|
||||
return s.fi.Sys()
|
||||
}
|
||||
|
||||
var _ fs.FileInfo = (*subFsFileInfo)(nil)
|
||||
@@ -0,0 +1,27 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NewSubFS(t *testing.T) {
|
||||
f := os.DirFS("test-fixtures/zip-fs")
|
||||
f = newSubFS(f, "github.com/someorg/somepkg@version")
|
||||
var names []string
|
||||
err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
names = append(names, path)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
expected := []string{
|
||||
".",
|
||||
"a-file",
|
||||
"subdir",
|
||||
"subdir/subfile",
|
||||
}
|
||||
require.Equal(t, expected, names)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright (c) 2022
|
||||
Acme, inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
THIS SOFTWARE IS PROVIDED BY [Name of Organization] “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL [Name of Organisation] BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+1
@@ -0,0 +1 @@
|
||||
a file
|
||||
+1
@@ -0,0 +1 @@
|
||||
another file
|
||||
Reference in New Issue
Block a user