github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/packagehandlers/pythonpackagehandler.go (about)

     1  package packagehandlers
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/jfrog/frogbot/utils"
     7  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  )
    13  
    14  const (
    15  
    16  	// Package names are case-insensitive with this prefix
    17  	PythonPackageRegexPrefix = "(?i)"
    18  	// Match all possible operators and versions syntax
    19  	PythonPackageRegexSuffix = "\\s*(([\\=\\<\\>\\~]=)|([\\>\\<]))\\s*(\\.|\\d)*(\\d|(\\.\\*))(\\,\\s*(([\\=\\<\\>\\~]=)|([\\>\\<])).*\\s*(\\.|\\d)*(\\d|(\\.\\*)))?"
    20  )
    21  
    22  // PythonPackageHandler Handles all the python package mangers as they share behavior
    23  type PythonPackageHandler struct {
    24  	pipRequirementsFile string
    25  	CommonPackageHandler
    26  }
    27  
    28  func (py *PythonPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error {
    29  	if vulnDetails.IsDirectDependency {
    30  		return py.updateDirectDependency(vulnDetails)
    31  	}
    32  
    33  	return &utils.ErrUnsupportedFix{
    34  		PackageName:  vulnDetails.ImpactedDependencyName,
    35  		FixedVersion: vulnDetails.SuggestedFixedVersion,
    36  		ErrorType:    utils.IndirectDependencyFixNotSupported,
    37  	}
    38  }
    39  
    40  func (py *PythonPackageHandler) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) {
    41  	switch vulnDetails.Technology {
    42  	case coreutils.Poetry:
    43  		return py.handlePoetry(vulnDetails)
    44  	case coreutils.Pip:
    45  		return py.handlePip(vulnDetails)
    46  	case coreutils.Pipenv:
    47  		return py.CommonPackageHandler.UpdateDependency(vulnDetails, vulnDetails.Technology.GetPackageInstallationCommand())
    48  	default:
    49  		return errors.New("unknown python package manger: " + vulnDetails.Technology.GetPackageType())
    50  	}
    51  }
    52  
    53  func (py *PythonPackageHandler) handlePoetry(vulnDetails *utils.VulnerabilityDetails) (err error) {
    54  	// Install the desired fixed version
    55  	if err = py.CommonPackageHandler.UpdateDependency(vulnDetails, vulnDetails.Technology.GetPackageInstallationCommand()); err != nil {
    56  		return
    57  	}
    58  	// Update Poetry lock file as well
    59  	return runPackageMangerCommand(coreutils.Poetry.GetExecCommandName(), coreutils.Poetry.String(), []string{"update"})
    60  }
    61  
    62  func (py *PythonPackageHandler) handlePip(vulnDetails *utils.VulnerabilityDetails) (err error) {
    63  	var fixedFile string
    64  	// This function assumes that the version of the dependencies is statically pinned in the requirements file or inside the 'install_requires' array in the setup.py file
    65  	fixedPackage := vulnDetails.ImpactedDependencyName + "==" + vulnDetails.SuggestedFixedVersion
    66  	if py.pipRequirementsFile == "" {
    67  		py.pipRequirementsFile = "setup.py"
    68  	}
    69  	wd, err := os.Getwd()
    70  	if err != nil {
    71  		return
    72  	}
    73  	fullPath := filepath.Join(wd, py.pipRequirementsFile)
    74  	if !strings.HasPrefix(filepath.Clean(fullPath), wd) {
    75  		return errors.New("wrong requirements file input")
    76  	}
    77  	data, err := os.ReadFile(filepath.Clean(py.pipRequirementsFile))
    78  	if err != nil {
    79  		return errors.New("an error occurred while attempting to read the requirements file:\n" + err.Error())
    80  	}
    81  	currentFile := string(data)
    82  
    83  	// Check both original and lowered package name and replace to only one lowered result
    84  	// This regex will match the impactedPackage with it's pinned version e.py. PyJWT==1.7.1
    85  	re := regexp.MustCompile(PythonPackageRegexPrefix + "(" + vulnDetails.ImpactedDependencyName + "|" + strings.ToLower(vulnDetails.ImpactedDependencyName) + ")" + PythonPackageRegexSuffix)
    86  	if packageToReplace := re.FindString(currentFile); packageToReplace != "" {
    87  		fixedFile = strings.Replace(currentFile, packageToReplace, strings.ToLower(fixedPackage), 1)
    88  	}
    89  	if fixedFile == "" {
    90  		return fmt.Errorf("impacted package %s not found, fix failed", vulnDetails.ImpactedDependencyName)
    91  	}
    92  	if err = os.WriteFile(py.pipRequirementsFile, []byte(fixedFile), 0600); err != nil {
    93  		err = fmt.Errorf("an error occured while writing the fixed version of %s to the requirements file:\n%s", vulnDetails.SuggestedFixedVersion, err.Error())
    94  	}
    95  	return
    96  }