Files
syft/syft/pkg/cataloger/python/parse_setup.go
T
4b8a3db5f3 Add nested archive cataloging (#5260)
Signed-off-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Kudryavcev Nikolay <kydry.nikolau@gmail.com>
Co-authored-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2026-10-29 17:33:07 -04:00

138 lines
4.2 KiB
Go

package python
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
type setupFileParser struct {
cfg CatalogerConfig
licenseResolver pythonLicenseResolver
}
func newSetupFileParser(cfg CatalogerConfig) setupFileParser {
return setupFileParser{
cfg: cfg,
licenseResolver: newPythonLicenseResolver(cfg),
}
}
// match examples:
//
// 'pathlib3==2.2.0;python_version<"3.6"' --> match(name=pathlib3 version=2.2.0)
// "mypy==v0.770", --> match(name=mypy version=v0.770)
// " mypy2 == v0.770", ' mypy3== v0.770', --> match(name=mypy2 version=v0.770), match(name=mypy3, version=v0.770)
var pinnedDependency = regexp.MustCompile(`['"]\W?(\w+\W?==\W?[\w.]*)`)
var unquotedPinnedDependency = regexp.MustCompile(`^\s*(\w+)\s*==\s*([\w\.\-]+)`)
func (sp setupFileParser) parseSetupFile(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var packages []pkg.Package
scanner := internal.NewLineScanner(reader)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimRight(line, "\n")
packages = sp.processQuotedDependencies(ctx, line, reader, packages)
packages = sp.processUnquotedDependency(ctx, line, reader, packages)
}
if err := scanner.Err(); err != nil {
return packages, nil, fmt.Errorf("unable to read setup.py: %w", err)
}
return packages, nil, nil
}
func (sp setupFileParser) processQuotedDependencies(ctx context.Context, line string, reader file.LocationReadCloser, packages []pkg.Package) []pkg.Package {
for _, match := range pinnedDependency.FindAllString(line, -1) {
if p, ok := sp.parseQuotedDependency(ctx, match, reader); ok {
packages = append(packages, p)
}
}
return packages
}
func (sp setupFileParser) parseQuotedDependency(ctx context.Context, match string, reader file.LocationReadCloser) (pkg.Package, bool) {
parts := strings.Split(match, "==")
if len(parts) != 2 {
return pkg.Package{}, false
}
name := cleanDependencyString(parts[0])
version := cleanDependencyString(parts[len(parts)-1])
return sp.validateAndCreatePackage(ctx, name, version, reader)
}
// processUnquotedDependency extracts and processes an unquoted dependency from a line
func (sp setupFileParser) processUnquotedDependency(ctx context.Context, line string, reader file.LocationReadCloser, packages []pkg.Package) []pkg.Package {
matches := unquotedPinnedDependency.FindStringSubmatch(line)
if len(matches) != 3 {
return packages
}
name := strings.TrimSpace(matches[1])
version := strings.TrimSpace(matches[2])
if p, ok := sp.validateAndCreatePackage(ctx, name, version, reader); ok {
if !isDuplicatePackage(p, packages) {
packages = append(packages, p)
}
}
return packages
}
func cleanDependencyString(s string) string {
s = strings.Trim(s, "'\"")
s = strings.TrimSpace(s)
s = strings.Trim(s, "'\"")
return s
}
func (sp setupFileParser) validateAndCreatePackage(ctx context.Context, name, version string, reader file.LocationReadCloser) (pkg.Package, bool) {
if hasTemplateDirective(name) || hasTemplateDirective(version) {
// this can happen in more dynamic setup.py where there is templating
return pkg.Package{}, false
}
if name == "" || version == "" {
// the raw line is not logged since this runs once per match, which is quadratic on a long line
log.WithFields("path", reader.FullNestedPath(), "name", name, "version", version).Debug("unable to parse package in setup.py")
return pkg.Package{}, false
}
p := newPackageForIndex(
ctx,
sp.licenseResolver,
name,
version,
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
return p, true
}
func isDuplicatePackage(p pkg.Package, packages []pkg.Package) bool {
for _, existing := range packages {
if existing.Name == p.Name && existing.Version == p.Version {
return true
}
}
return false
}
func hasTemplateDirective(s string) bool {
return strings.Contains(s, `%s`) || strings.Contains(s, `{`) || strings.Contains(s, `}`)
}