-
Notifications
You must be signed in to change notification settings - Fork 89
Refactor NPM package updater #1024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3_er
Are you sure you want to change the base?
Refactor NPM package updater #1024
Conversation
… location extraction for a given VulnerabilityDetails
…r-npm-package-updater
|
please provide a link to a fix pr |
eyalk007
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reviewed most of files
please tell me you are done so i can rereveiw
| regexpCompleteFormat := fmt.Sprintf(strings.ToLower(dependencyLineFormat), regexpFitImpactedName, regexpFitImpactedVersion) | ||
| return regexp.MustCompile(regexpCompleteFormat) | ||
| } | ||
|
|
||
| // Extracts unique file paths from the vulnerability's component locations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove comment
| // Matches: "package-name": "version" with optional ^ or ~ prefix | ||
| npmDependencyRegexpPattern = `\s*"%s"\s*:\s*"[~^]?%s"` | ||
| // Regex pattern for replacement - captures the groups for reconstruction | ||
| npmDependencyReplacePattern = `(\s*"%s"\s*:\s*")[~^]?[^"]+(")` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regex patterns are self explanatory
no need for comments
| func (npm *NpmPackageHandler) getDescriptorsToFixFromVulnerability(vulnDetails *utils.VulnerabilityDetails) ([]string, error) { | ||
| lockFilePaths := GetVulnerabilityLocations(vulnDetails) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rename it to getDescriptorsPaths
Also we said its a bug, so that's why we need to do this
so basically we need to delete this function once they fix the bug?
if so add a todo of deleting the function
also delete comment after rename as it will be self explanatory
So something like:
// TODO: This is a workaround. Engine provides lock file paths but we need descriptor paths.
// Delete this function once engine provides descriptor paths directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this would be changed once the bug will be fixed. I coded it before we knew its a but and I maintained it since we dont know when the bug will be fixed. Ill add the comment
| // Change to the descriptor directory for the regeneration of the lock file | ||
| descriptorDir := filepath.Dir(descriptorPath) | ||
| if err = os.Chdir(descriptorDir); err != nil { | ||
| return fmt.Errorf("failed to change directory to '%s': %w", descriptorDir, err) | ||
| } | ||
| defer func() { | ||
| if chErr := os.Chdir(originalWd); chErr != nil { | ||
| err = errors.Join(err, fmt.Errorf("failed to return to original directory: %w", chErr)) | ||
| } | ||
| }() | ||
|
|
||
| if err = npm.regenerateLockFileWithRetry(); err != nil { | ||
| log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) | ||
| if rollbackErr := os.WriteFile(descriptorPath, backupContent, 0644); rollbackErr != nil { | ||
| return fmt.Errorf("failed to rollback descriptor after lock file regeneration failure: %w (original error: %v)", rollbackErr, err) | ||
| } | ||
| return err | ||
| } | ||
|
|
||
| log.Debug(fmt.Sprintf("Successfully updated '%s' from version '%s' to '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, vulnDetails.SuggestedFixedVersion)) | ||
| return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
separation of concerns
the goal of this func is to update the descriptor
this func is both updating the descriptor and tidying up the lockfile
I would separate to 2 function calls in the loop
and would check with product regarding backup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really. the function that is updating the descriptor is called updateVersionInDescriptor. this is why I called this function fixVulnerabilityInDescriptor as it fixes vulnerability and performing all the steps around it.
we can change it to 'fixVulnerability' if you still think it is not clear enough
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| descriptorDir := filepath.Dir(descriptorPath) | ||
| if err = os.Chdir(descriptorDir); err != nil { | ||
| return fmt.Errorf("failed to change directory to '%s': %w", descriptorDir, err) | ||
| } | ||
| defer func() { | ||
| if chErr := os.Chdir(originalWd); chErr != nil { | ||
| err = errors.Join(err, fmt.Errorf("failed to return to original directory: %w", chErr)) | ||
| } | ||
| }() | ||
|
|
||
| if err = npm.regenerateLockFileWithRetry(); err != nil { | ||
| log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) | ||
| if rollbackErr := os.WriteFile(descriptorPath, backupContent, 0644); rollbackErr != nil { | ||
| return fmt.Errorf("failed to rollback descriptor after lock file regeneration failure: %w (original error: %v)", rollbackErr, err) | ||
| } | ||
| return err | ||
| } | ||
|
|
||
| log.Debug(fmt.Sprintf("Successfully updated '%s' from version '%s' to '%s'", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, vulnDetails.SuggestedFixedVersion)) | ||
| return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if user doesnt have lockfile in git, are we taking this into consideration?
@orto17
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree - we talked about it before and as far as I remember this is what we decided.
In V2 we have a logic of 'cleaning' whatever that is not in the remote (like node_modules for example).
we can apply same logic here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @@ -0,0 +1,297 @@ | |||
| package packagehandlers | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing test cases:
-
Version Range Mismatch - meaning lockfile resolves to diff version
-
Multiple Occurrences - multiple occurrence of the same version once as lets say a dep and once as a dev dep
-
Diff types of deps - we have peer deps, deps, dev deps, peerDeps, optionalDeps
plus we have overrides -
Maybe a test for the rollback functionality if we keep it
| } | ||
|
|
||
| // Extracts unique file paths from the vulnerability's component locations. | ||
| func GetVulnerabilityLocations(vulnDetails *utils.VulnerabilityDetails) []string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i though we want to delete the common package updater completely. i dont think its same to have both utils file and common package updater file. My suggestion is to have a "package updaters utils", and have all new generic logic added to there.
meaning, at the end of the refactor, there will be no usage in common package updater at all.
|
|
||
| // Extracts unique file paths from the vulnerability's component locations. | ||
| func GetVulnerabilityLocations(vulnDetails *utils.VulnerabilityDetails) []string { | ||
| pathsSet := datastructures.MakeSet[string]() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also it looks very similar to getPomPaths function from the Maven refactor. so i suggest @eyalk007 will make it generic and @eranturgeman will use the new generic function in utils.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think we should write new utils tests in a nee file named to utils_test, so it ill not mix with the current
specific tech tests, it will be eaisier to review and follow the "v3 tests" vs the "v2 tests"
| npmDependencyReplacePattern = `(\s*"%s"\s*:\s*")[~^]?[^"]+(")` | ||
| ) | ||
|
|
||
| var npmInstallEnvVars = map[string]string{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this map specific to npm? looks like we can extract it to utils and have all relevant updaters use it
| noUpdateNotifierEnv: "1", | ||
| } | ||
|
|
||
| type NpmPackageHandler struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NpmPackageUpdater
and lets delete CommonPackageHandler and use utils functions instead
| } | ||
|
|
||
| // Returns all descriptors related to the vulnerability based on its lock file locations | ||
| func (npm *NpmPackageHandler) getDescriptorsToFixFromVulnerability(vulnDetails *utils.VulnerabilityDetails) ([]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo - delete when Avi fixes the descriptor bug
also delete TestNpmGetDescriptorPathsFromVulnerability (even though it looks very good)
| return err | ||
| } | ||
|
|
||
| originalWd, err := os.Getwd() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
originalWd or currentWd?
|
|
||
| for _, descriptorPath := range descriptorPaths { | ||
| if fixErr := npm.fixVulnerabilityInDescriptor(vulnDetails, descriptorPath, originalWd, vulnRegexp); fixErr != nil { | ||
| err = errors.Join(err, fixErr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add a warn log here in case of an error?
| // This function adjusts the name and version of a dependency to conform to a regular expression format and constructs the complete regular expression pattern for searching. | ||
| // Note: 'dependencyLineFormat' should be a template with two placeholders to be populated. The first one will be replaced with 'impactedName', and the second one with 'impactedVersion'. | ||
| // Note: All supplied arguments are converted to lowercase. Hence, when utilizing this function, the file in which we search for the patterns must also be converted to lowercase. | ||
| // Note: This function may not support all package manager dependency formats. It is designed for package managers where the dependency's name consists of a single component. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this comment is redundant including the notes
| return fmt.Errorf("failed to get current working directory: %w", err) | ||
| } | ||
|
|
||
| vulnRegexp := GetVulnerabilityRegexCompiler(vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, npmDependencyRegexpPattern) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cant there be a scenario where the regex is invalid?
| } | ||
| } | ||
| if err != nil { | ||
| return fmt.Errorf("failed to fix vulnerability in one of the following descriptors [%s]: %w", strings.Join(descriptorPaths, ", "), err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this log message/error message is somehow too "aggressive" to my opinion. if some of the fixes succeeded, it looks bad when we add a "fail to fix..." log. can we change the message to something more gentle?
| func (npm *NpmPackageHandler) fixVulnerabilityInDescriptor(vulnDetails *utils.VulnerabilityDetails, descriptorPath string, originalWd string, vulnRegexp *regexp.Regexp) (err error) { | ||
| descriptorContent, err := os.ReadFile(descriptorPath) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to read file '%s': %w", descriptorPath, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for each error/warn message i think is it should be more informative on from where the error is, for example:
fmt.Errorf("failed to fix vulnerability in descriptor: failed to read file '%s': %w", descriptorPath, err)
|
|
||
| if err = npm.regenerateLockFileWithRetry(); err != nil { | ||
| log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion, err.Error())) | ||
| if rollbackErr := os.WriteFile(descriptorPath, backupContent, 0644); rollbackErr != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i dont think we should roll back. i think we should add a warning log/comment that the lock file was not updated. and have them the fix PR with only the descriptor changed.
| return updatedContent, nil | ||
| } | ||
|
|
||
| func (npm *NpmPackageHandler) regenerateLockFileWithRetry() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the func structure should be:
if err != nil {
log.Debug(fmt.Sprintf("First npm install attempt failed: %s. Retrying...", err.Error()))
if err = npm.runNpmInstall(); err != nil {
return fmt.Errorf("npm install failed after retry: %w", err)
}
......
}

This PR changes the NPM package handler to test-based fixes instead of cli command fixes.
As past of the change we ease the installation after a fix is performed to only regenerate the lock file, hence reducing the strict build process we used to have and make the process less error prone