Expand python license scanning to cover unclaimed files (#3779)

* expand python license scanning to cover unclaimed files

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* speed up tests using the license scanner

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2025-04-03 10:31:02 -04:00
committed by GitHub
parent da62a82413
commit f851085668
39 changed files with 729 additions and 302 deletions
@@ -6,7 +6,12 @@ import (
"encoding/json"
"fmt"
"io"
"path"
"path/filepath"
"sort"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/licenses"
@@ -247,3 +252,97 @@ func assembleEggOrWheelMetadata(resolver file.Resolver, metadataLocation file.Lo
pd.DirectURLOrigin = d
return &pd, sources, nil
}
func findLicenses(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, m parsedData) pkg.LicenseSet {
var licenseSet pkg.LicenseSet
licenseLocations := file.NewLocationSet()
if m.LicenseFilePath != "" {
locs, err := resolver.FilesByPath(m.LicenseFilePath)
if err != nil {
log.WithFields("error", err, "path", m.LicenseFilePath).Trace("unable to resolve python license file")
} else {
licenseLocations.Add(locs...)
}
}
switch {
case m.LicenseExpression != "" || m.Licenses != "":
licenseSet = getLicenseSetFromValues(licenseLocations.ToSlice(), m.LicenseExpression, m.Licenses)
case !licenseLocations.Empty():
licenseSet = getLicenseSetFromFiles(ctx, scanner, resolver, licenseLocations.ToSlice()...)
default:
// search for known license paths from RECORDS file
licenseNames := strset.New()
for _, n := range licenses.FileNames() {
licenseNames.Add(strings.ToLower(n))
}
parent := path.Base(path.Dir(m.DistInfoLocation.Path()))
candidatePaths := strset.New()
for _, f := range m.Files {
if !strings.HasPrefix(f.Path, parent) || strings.Count(f.Path, "/") > 1 {
continue
}
if licenseNames.Has(strings.ToLower(filepath.Base(f.Path))) {
candidatePaths.Add(path.Join(m.SitePackagesRootPath, f.Path))
}
}
paths := candidatePaths.List()
sort.Strings(paths)
locationSet := file.NewLocationSet()
for _, p := range paths {
locs, err := resolver.FilesByPath(p)
if err != nil {
log.WithFields("error", err, "path", p).Trace("unable to resolve python license in dist-info")
continue
}
locationSet.Add(locs...)
}
licenseSet = getLicenseSetFromFiles(ctx, scanner, resolver, locationSet.ToSlice()...)
}
return licenseSet
}
func getLicenseSetFromValues(locations []file.Location, licenseValues ...string) pkg.LicenseSet {
if len(locations) == 0 {
return pkg.NewLicenseSet(pkg.NewLicensesFromValues(licenseValues...)...)
}
licenseSet := pkg.NewLicenseSet()
for _, value := range licenseValues {
if value == "" {
continue
}
licenseSet.Add(pkg.NewLicenseFromLocations(value, locations...))
}
return licenseSet
}
func getLicenseSetFromFiles(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, locations ...file.Location) pkg.LicenseSet {
licenseSet := pkg.NewLicenseSet()
for _, loc := range locations {
licenseSet.Add(getLicenseSetFromFile(ctx, scanner, resolver, loc)...)
}
return licenseSet
}
func getLicenseSetFromFile(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, location file.Location) []pkg.License {
metadataContents, err := resolver.FileContentsByLocation(location)
if err != nil {
log.WithFields("error", err, "path", location.Path()).Trace("unable to read file contents")
return nil
}
defer internal.CloseAndLogError(metadataContents, location.Path())
parsed, err := scanner.PkgSearch(ctx, file.NewLocationReadCloser(location, metadataContents))
if err != nil {
log.WithFields("error", err, "path", location.Path()).Trace("unable to parse a license from the file")
return nil
}
return parsed
}