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

     1  package packagehandlers
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/jfrog/frogbot/utils"
     7  	"io/fs"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  )
    14  
    15  const (
    16  	dotnetUpdateCmdPackageExtraArg        = "package"
    17  	dotnetNoRestoreFlag                   = "--no-restore"
    18  	dotnetAssetsFilesSuffix               = "csproj"
    19  	dotnetDependencyRegexpLowerCaseFormat = "include=[\\\"|\\']%s[\\\"|\\']\\s*version=[\\\"|\\']%s[\\\"|\\']"
    20  )
    21  
    22  type NugetPackageHandler struct {
    23  	CommonPackageHandler
    24  }
    25  
    26  func (nph *NugetPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error {
    27  	if vulnDetails.IsDirectDependency {
    28  		return nph.updateDirectDependency(vulnDetails)
    29  	}
    30  
    31  	return &utils.ErrUnsupportedFix{
    32  		PackageName:  vulnDetails.ImpactedDependencyName,
    33  		FixedVersion: vulnDetails.SuggestedFixedVersion,
    34  		ErrorType:    utils.IndirectDependencyFixNotSupported,
    35  	}
    36  }
    37  
    38  func (nph *NugetPackageHandler) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) {
    39  	var assetsFilePaths []string
    40  	assetsFilePaths, err = getAssetsFilesPaths()
    41  	if err != nil {
    42  		return
    43  	}
    44  
    45  	wd, err := os.Getwd()
    46  	if err != nil {
    47  		err = fmt.Errorf("failed to get current working directory: %s", err.Error())
    48  		return
    49  	}
    50  
    51  	vulnRegexpCompiler := getVulnerabilityRegexCompiler(vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion)
    52  	var isAnyFileChanged bool
    53  
    54  	for _, assetFilePath := range assetsFilePaths {
    55  		var isFileChanged bool
    56  		isFileChanged, err = nph.fixVulnerabilityIfExists(vulnDetails, assetFilePath, vulnRegexpCompiler, wd)
    57  		if err != nil {
    58  			err = fmt.Errorf("failed to update asset file '%s': %s", assetFilePath, err.Error())
    59  			return
    60  		}
    61  
    62  		// We use logic OR in order to keep track whether any asset file changed during the fix process
    63  		isAnyFileChanged = isAnyFileChanged || isFileChanged
    64  	}
    65  
    66  	if !isAnyFileChanged {
    67  		err = fmt.Errorf("impacted package '%s' was not found or could not be fixed in all descriptor files", vulnDetails.ImpactedDependencyName)
    68  	}
    69  	return
    70  }
    71  
    72  func getAssetsFilesPaths() (assetsFilePaths []string, err error) {
    73  	err = filepath.WalkDir(".", func(path string, d fs.DirEntry, innerErr error) error {
    74  		if innerErr != nil {
    75  			return fmt.Errorf("an error has occurred when attempting to access or traverse the file system: %s", innerErr.Error())
    76  		}
    77  
    78  		if strings.HasSuffix(path, dotnetAssetsFilesSuffix) {
    79  			var absFilePath string
    80  			absFilePath, innerErr = filepath.Abs(path)
    81  			if innerErr != nil {
    82  				return fmt.Errorf("couldn't retrieve file's absolute path for './%s': %s", path, innerErr.Error())
    83  			}
    84  			assetsFilePaths = append(assetsFilePaths, absFilePath)
    85  		}
    86  		return nil
    87  	})
    88  	return
    89  }
    90  
    91  func (nph *NugetPackageHandler) fixVulnerabilityIfExists(vulnDetails *utils.VulnerabilityDetails, assetFilePath string, vulnRegexpCompiler *regexp.Regexp, originalWd string) (isFileChanged bool, err error) {
    92  	modulePath := path.Dir(assetFilePath)
    93  
    94  	var fileData []byte
    95  	fileData, err = os.ReadFile(assetFilePath)
    96  	if err != nil {
    97  		err = fmt.Errorf("failed to read file '%s': %s", assetFilePath, err.Error())
    98  		return
    99  	}
   100  	fileContent := strings.ToLower(string(fileData))
   101  
   102  	if matchingRow := vulnRegexpCompiler.FindString(fileContent); matchingRow != "" {
   103  		err = os.Chdir(modulePath)
   104  		if err != nil {
   105  			err = fmt.Errorf("failed to change directory to '%s': %s", modulePath, err.Error())
   106  			return
   107  		}
   108  		defer func() {
   109  			err = errors.Join(err, os.Chdir(originalWd))
   110  		}()
   111  
   112  		err = nph.CommonPackageHandler.UpdateDependency(vulnDetails, vulnDetails.Technology.GetPackageInstallationCommand(), dotnetUpdateCmdPackageExtraArg, dotnetNoRestoreFlag)
   113  		if err != nil {
   114  			return
   115  		}
   116  		isFileChanged = true
   117  	}
   118  	return
   119  }
   120  
   121  func getVulnerabilityRegexCompiler(impactedName string, impactedVersion string) *regexp.Regexp {
   122  	// We replace '.' with '\\.' since '.' is a special character in regexp patterns, and we want to capture the character '.' itself
   123  	// To avoid dealing with case sensitivity we lower all characters in the package's name and in the file we check
   124  	regexpFitImpactedName := strings.ToLower(strings.ReplaceAll(impactedName, ".", "\\."))
   125  	regexpFitImpactedVersion := strings.ToLower(strings.ReplaceAll(impactedVersion, ".", "\\."))
   126  	regexpCompleteFormat := fmt.Sprintf(dotnetDependencyRegexpLowerCaseFormat, regexpFitImpactedName, regexpFitImpactedVersion)
   127  	return regexp.MustCompile(regexpCompleteFormat)
   128  }