Files
syft/syft/pkg/cataloger/golang/license_finder.go
T
2025-11-03 14:16:05 -05:00

153 lines
4.3 KiB
Go

package golang
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/spf13/afero"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/licenses"
)
// resolveModuleLicenses finds and parses license files for Go modules
func resolveModuleLicenses(ctx context.Context, scanRoot string, pkgInfos []pkgInfo, fs afero.Fs) pkg.LicenseSet {
out := pkg.NewLicenseSet()
for _, info := range pkgInfos {
modDir, pkgDir, err := getAbsolutePkgPaths(info)
if err != nil {
continue
}
licenseFiles, err := findAllLicenseCandidatesUpwards(pkgDir, modDir, fs)
if err != nil {
continue
}
for _, f := range licenseFiles {
out.Add(readLicenses(ctx, scanRoot, fs, f)...)
}
}
return out
}
func readLicenses(ctx context.Context, scanRoot string, fs afero.Fs, f string) []pkg.License {
contents, err := fs.Open(f)
if err != nil {
log.WithFields("file", f, "error", err).Debug("unable to read license file")
return nil
}
defer internal.CloseAndLogError(contents, f)
location := file.Location{}
if scanRoot != "" && strings.HasPrefix(f, scanRoot) {
// include location when licenses are found within the scan target
location = file.NewLocation(strings.TrimPrefix(f, scanRoot))
}
return pkg.NewLicensesFromReadCloserWithContext(ctx, file.NewLocationReadCloser(location, contents))
}
/*
findAllLicenseCandidatesUpwards performs a bubble-up search per package:
1. pkgInfos represents a sparse vertical distribution of packages within modules
2. we get more pkgInfos for free when the build configuration is updated
The recursion terminates via two conditions:
- When dir is outside stopAt boundary (happy case)
- When reaching filesystem root where parent == dir (edge case)
Note: The code does NOT follow symlinks. It returns a slice of absolute paths that
represent license file matches that are resolved independently of the bubble-up.
When we should consider redesign tip to stem:
- Reduced filesystem calls: Single traversal vs multiple per-package
- Path deduplication: Avoids re-scanning common parent directories
- Better for wide module structures: Efficient when many packages share parent paths
- We need to consider the case here where nested modules are visited by accident and licenses
are erroneously associated to a 'parent module'; bubble up currently prevents this
*/
func findAllLicenseCandidatesUpwards(dir string, stopAt string, fs afero.Fs) ([]string, error) {
// Validate that both paths are absolute
if !filepath.IsAbs(dir) {
return nil, fmt.Errorf("dir must be an absolute path, got: %s", dir)
}
if !filepath.IsAbs(stopAt) {
return nil, fmt.Errorf("stopAt must be an absolute path, got: %s", stopAt)
}
return findLicenseCandidates(dir, stopAt, fs)
}
func findLicenseCandidates(dir string, stopAt string, fs afero.Fs) ([]string, error) {
// stop if we've gone outside the stopAt directory
if !strings.HasPrefix(dir, stopAt) {
return []string{}, nil
}
out, err := findLicensesInDir(dir, fs)
if err != nil {
return nil, err
}
parent := filepath.Dir(dir)
// can't go any higher up the directory tree: "/" case
if parent == dir {
return out, nil
}
// search parent directory and combine results
parentLicenses, err := findLicenseCandidates(parent, stopAt, fs)
if err != nil {
return nil, err
}
// Combine current directory licenses with parent directory licenses
return append(out, parentLicenses...), nil
}
func getAbsolutePkgPaths(info pkgInfo) (modDir string, pkgDir string, err error) {
pkgDir, err = filepath.Abs(info.pkgDir)
if err != nil {
return modDir, pkgDir, err
}
modDir, err = filepath.Abs(info.moduleDir)
if err != nil {
return modDir, pkgDir, err
}
if !strings.HasPrefix(pkgDir, modDir) {
return modDir, pkgDir, fmt.Errorf("modDir %s should contain pkgDir %s", modDir, pkgDir)
}
return modDir, pkgDir, nil
}
func findLicensesInDir(dir string, fs afero.Fs) ([]string, error) {
var out []string
dirContents, err := afero.ReadDir(fs, dir)
if err != nil {
return nil, err
}
for _, f := range dirContents {
if f.IsDir() {
continue
}
if licenses.IsLicenseFile(f.Name()) {
path := filepath.Join(dir, f.Name())
out = append(out, path)
}
}
return out, nil
}