github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/python/virtual_env.go (about) 1 package python 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "path" 8 "sort" 9 "strings" 10 11 "github.com/bmatcuk/doublestar/v4" 12 "github.com/scylladb/go-set/strset" 13 14 "github.com/anchore/syft/internal" 15 "github.com/anchore/syft/internal/log" 16 "github.com/anchore/syft/syft/file" 17 ) 18 19 type virtualEnvInfo struct { 20 // Context 21 Location file.Location 22 SitePackagesPath string 23 24 // Config values 25 Version string 26 IncludeSystemSitePackages bool 27 } 28 29 func (v virtualEnvInfo) majorMinorVersion() string { 30 parts := strings.Split(v.Version, ".") 31 if len(parts) < 2 { 32 return "" 33 } 34 return strings.Join(parts[:2], ".") 35 } 36 37 func findVirtualEnvs(_ context.Context, resolver file.Resolver, sitePackagePaths []string) ([]virtualEnvInfo, []string, error) { 38 locations, err := resolver.FilesByGlob("**/pyvenv.cfg") 39 if err != nil { 40 return nil, nil, fmt.Errorf("failed to find python virtualenvs: %w", err) 41 } 42 43 sitePackagePathsSet := strset.New(sitePackagePaths...) 44 45 var virtualEnvs []virtualEnvInfo 46 for _, location := range locations { 47 cfg, err := parsePyvenvCfg(context.Background(), resolver, location) 48 if err != nil { 49 return nil, nil, fmt.Errorf("failed to parse pyvenv.cfg: %w", err) 50 } 51 if cfg == nil { 52 continue 53 } 54 55 cfg.SitePackagesPath = cfg.matchVirtualEnvSitePackagesPath(sitePackagePaths) 56 57 if cfg.SitePackagesPath != "" { 58 sitePackagePathsSet.Remove(cfg.SitePackagesPath) 59 } 60 61 virtualEnvs = append(virtualEnvs, *cfg) 62 } 63 64 unusedSitePackageDirs := sitePackagePathsSet.List() 65 sort.Strings(unusedSitePackageDirs) 66 67 return virtualEnvs, unusedSitePackageDirs, nil 68 } 69 70 func (v virtualEnvInfo) matchSystemPackagesPath(sitePackagePaths []string) string { 71 sitePackagePathsSet := strset.New(sitePackagePaths...) 72 73 // we are searchin for the system site-packages directory within the virtualenv 74 search := "**/python" + v.majorMinorVersion() + "/*-packages" 75 76 var matches []string 77 for _, p := range sitePackagePathsSet.List() { 78 doesMatch, err := doublestar.Match(search, p) 79 if err != nil { 80 log.Tracef("unable to match system site-packages path %q: %v", p, err) 81 continue 82 } 83 if doesMatch { 84 matches = append(matches, p) 85 } 86 } 87 88 // we should get either 0 or 1 matches, we cannot reason about multiple matches 89 if len(matches) == 1 { 90 return matches[0] 91 } 92 93 return "" 94 } 95 96 func (v virtualEnvInfo) matchVirtualEnvSitePackagesPath(sitePackagePaths []string) string { 97 sitePackagePathsSet := strset.New(sitePackagePaths...) 98 // the parent directory of the venv config is the top-level directory of the virtualenv 99 // e.g. /app/project1/venv/pyvenv.cfg -> /app/project1/venv 100 parent := strings.TrimLeft(path.Dir(v.Location.RealPath), "/") 101 102 // we are searchin for the site-packages directory within the virtualenv 103 search := parent + "/lib/python" + v.majorMinorVersion() + "/site-packages" 104 105 var matches []string 106 for _, p := range sitePackagePathsSet.List() { 107 if strings.Contains(p, search) { 108 matches = append(matches, p) 109 } 110 } 111 112 // we should get either 0 or 1 matches, we cannot reason about multiple matches 113 if len(matches) == 1 { 114 return matches[0] 115 } 116 117 return "" 118 } 119 120 func parsePyvenvCfg(_ context.Context, resolver file.Resolver, location file.Location) (*virtualEnvInfo, error) { 121 reader, err := resolver.FileContentsByLocation(location) 122 if err != nil { 123 return nil, fmt.Errorf("unable to read file %q: %w", location.Path(), err) 124 } 125 defer internal.CloseAndLogError(reader, location.Path()) 126 127 cfg, err := parsePyvenvCfgReader(file.NewLocationReadCloser(location, reader)) 128 if err != nil { 129 return nil, fmt.Errorf("unable to parse pyvenv.cfg: %w", err) 130 } 131 132 return cfg, nil 133 } 134 135 func parsePyvenvCfgReader(reader file.LocationReadCloser) (*virtualEnvInfo, error) { 136 scanner := bufio.NewScanner(reader) 137 138 venv := virtualEnvInfo{ 139 Location: reader.Location, 140 } 141 142 for scanner.Scan() { 143 line := scanner.Text() 144 line = strings.TrimSpace(line) 145 if line == "" || strings.HasPrefix(line, "#") { 146 // skip empty lines and comments 147 continue 148 } 149 150 parts := strings.SplitN(line, "=", 2) 151 if len(parts) != 2 { 152 // skip malformed lines 153 continue 154 } 155 156 key := strings.TrimSpace(parts[0]) 157 value := strings.TrimSpace(parts[1]) 158 159 switch key { 160 case "version": 161 venv.Version = value 162 case "include-system-site-packages": 163 venv.IncludeSystemSitePackages = strings.ToLower(value) == "true" 164 } 165 } 166 167 if err := scanner.Err(); err != nil { 168 return nil, err 169 } 170 171 return &venv, nil 172 }