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  }