github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/python/parse_setup.go (about)

     1  package python
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/anchore/syft/internal/log"
    10  	"github.com/anchore/syft/syft/artifact"
    11  	"github.com/anchore/syft/syft/file"
    12  	"github.com/anchore/syft/syft/pkg"
    13  	"github.com/anchore/syft/syft/pkg/cataloger/generic"
    14  )
    15  
    16  type setupFileParser struct {
    17  	cfg             CatalogerConfig
    18  	licenseResolver pythonLicenseResolver
    19  }
    20  
    21  func newSetupFileParser(cfg CatalogerConfig) setupFileParser {
    22  	return setupFileParser{
    23  		cfg:             cfg,
    24  		licenseResolver: newPythonLicenseResolver(cfg),
    25  	}
    26  }
    27  
    28  // match examples:
    29  //
    30  //	'pathlib3==2.2.0;python_version<"3.6"'  --> match(name=pathlib3 version=2.2.0)
    31  //	 "mypy==v0.770",                        --> match(name=mypy version=v0.770)
    32  //	" mypy2 == v0.770", ' mypy3== v0.770',  --> match(name=mypy2 version=v0.770), match(name=mypy3, version=v0.770)
    33  var pinnedDependency = regexp.MustCompile(`['"]\W?(\w+\W?==\W?[\w.]*)`)
    34  var unquotedPinnedDependency = regexp.MustCompile(`^\s*(\w+)\s*==\s*([\w\.\-]+)`)
    35  
    36  func (sp setupFileParser) parseSetupFile(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
    37  	var packages []pkg.Package
    38  
    39  	scanner := bufio.NewScanner(reader)
    40  
    41  	for scanner.Scan() {
    42  		line := scanner.Text()
    43  		line = strings.TrimRight(line, "\n")
    44  
    45  		packages = sp.processQuotedDependencies(ctx, line, reader, packages)
    46  		packages = sp.processUnquotedDependency(ctx, line, reader, packages)
    47  	}
    48  
    49  	return packages, nil, nil
    50  }
    51  
    52  func (sp setupFileParser) processQuotedDependencies(ctx context.Context, line string, reader file.LocationReadCloser, packages []pkg.Package) []pkg.Package {
    53  	for _, match := range pinnedDependency.FindAllString(line, -1) {
    54  		if p, ok := sp.parseQuotedDependency(ctx, match, line, reader); ok {
    55  			packages = append(packages, p)
    56  		}
    57  	}
    58  	return packages
    59  }
    60  
    61  func (sp setupFileParser) parseQuotedDependency(ctx context.Context, match, line string, reader file.LocationReadCloser) (pkg.Package, bool) {
    62  	parts := strings.Split(match, "==")
    63  	if len(parts) != 2 {
    64  		return pkg.Package{}, false
    65  	}
    66  
    67  	name := cleanDependencyString(parts[0])
    68  	version := cleanDependencyString(parts[len(parts)-1])
    69  
    70  	return sp.validateAndCreatePackage(ctx, name, version, line, reader)
    71  }
    72  
    73  // processUnquotedDependency extracts and processes an unquoted dependency from a line
    74  func (sp setupFileParser) processUnquotedDependency(ctx context.Context, line string, reader file.LocationReadCloser, packages []pkg.Package) []pkg.Package {
    75  	matches := unquotedPinnedDependency.FindStringSubmatch(line)
    76  	if len(matches) != 3 {
    77  		return packages
    78  	}
    79  
    80  	name := strings.TrimSpace(matches[1])
    81  	version := strings.TrimSpace(matches[2])
    82  
    83  	if p, ok := sp.validateAndCreatePackage(ctx, name, version, line, reader); ok {
    84  		if !isDuplicatePackage(p, packages) {
    85  			packages = append(packages, p)
    86  		}
    87  	}
    88  
    89  	return packages
    90  }
    91  
    92  func cleanDependencyString(s string) string {
    93  	s = strings.Trim(s, "'\"")
    94  	s = strings.TrimSpace(s)
    95  	s = strings.Trim(s, "'\"")
    96  	return s
    97  }
    98  
    99  func (sp setupFileParser) validateAndCreatePackage(ctx context.Context, name, version, line string, reader file.LocationReadCloser) (pkg.Package, bool) {
   100  	if hasTemplateDirective(name) || hasTemplateDirective(version) {
   101  		// this can happen in more dynamic setup.py where there is templating
   102  		return pkg.Package{}, false
   103  	}
   104  
   105  	if name == "" || version == "" {
   106  		log.WithFields("path", reader.RealPath).Debugf("unable to parse package in setup.py line: %q", line)
   107  		return pkg.Package{}, false
   108  	}
   109  
   110  	p := newPackageForIndex(
   111  		ctx,
   112  		sp.licenseResolver,
   113  		name,
   114  		version,
   115  		reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
   116  	)
   117  
   118  	return p, true
   119  }
   120  
   121  func isDuplicatePackage(p pkg.Package, packages []pkg.Package) bool {
   122  	for _, existing := range packages {
   123  		if existing.Name == p.Name && existing.Version == p.Version {
   124  			return true
   125  		}
   126  	}
   127  	return false
   128  }
   129  
   130  func hasTemplateDirective(s string) bool {
   131  	return strings.Contains(s, `%s`) || strings.Contains(s, `{`) || strings.Contains(s, `}`)
   132  }