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 }