From aa9c313f5dece55b9a029c6af639d2af9272fc07 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 7 Jan 2026 11:44:13 +0200 Subject: [PATCH 1/8] refactoring npm package handler to use text based fixes and lock file regeneration --- packagehandlers/npmpackagehandler.go | 57 ------- packagehandlers/npmpackageupdater.go | 233 +++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 57 deletions(-) delete mode 100644 packagehandlers/npmpackagehandler.go create mode 100644 packagehandlers/npmpackageupdater.go diff --git a/packagehandlers/npmpackagehandler.go b/packagehandlers/npmpackagehandler.go deleted file mode 100644 index 1379b814e..000000000 --- a/packagehandlers/npmpackagehandler.go +++ /dev/null @@ -1,57 +0,0 @@ -package packagehandlers - -import ( - "errors" - "fmt" - "github.com/jfrog/frogbot/v2/utils" - npmCommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/npm" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" -) - -const ( - npmInstallPackageLockOnlyFlag = "--package-lock-only" - npmInstallIgnoreScriptsFlag = "--ignore-scripts" -) - -type NpmPackageHandler struct { - CommonPackageHandler -} - -func (npm *NpmPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { - if vulnDetails.IsDirectDependency { - return npm.updateDirectDependency(vulnDetails) - } else { - return &utils.ErrUnsupportedFix{ - PackageName: vulnDetails.ImpactedDependencyName, - FixedVersion: vulnDetails.SuggestedFixedVersion, - ErrorType: utils.IndirectDependencyFixNotSupported, - } - } -} - -func (npm *NpmPackageHandler) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) (err error) { - isNodeModulesExists, err := fileutils.IsDirExists("node_modules", false) - if err != nil { - err = fmt.Errorf("failed while serching for node_modules in project: %s", err.Error()) - return - } - - commandFlags := []string{npmInstallIgnoreScriptsFlag} - if !isNodeModulesExists { - // In case node_modules don't exist in current dir the fix will update only package.json and package-lock.json - commandFlags = append(commandFlags, npmInstallPackageLockOnlyFlag) - } - - // Configure resolution from an Artifactory server if needed - if npm.depsRepo != "" { - var clearResolutionServerFunc func() error - clearResolutionServerFunc, err = npmCommand.SetArtifactoryAsResolutionServer(npm.serverDetails, npm.depsRepo) - if err != nil { - return - } - defer func() { - err = errors.Join(err, clearResolutionServerFunc()) - }() - } - return npm.CommonPackageHandler.UpdateDependency(vulnDetails, vulnDetails.Technology.GetPackageInstallationCommand(), commandFlags...) -} diff --git a/packagehandlers/npmpackageupdater.go b/packagehandlers/npmpackageupdater.go new file mode 100644 index 000000000..7ebff692b --- /dev/null +++ b/packagehandlers/npmpackageupdater.go @@ -0,0 +1,233 @@ +package packagehandlers + +import ( + "context" + "errors" + "fmt" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/jfrog/frogbot/v2/utils" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +const ( + npmPackageLockOnlyFlag = "--package-lock-only" + npmIgnoreScriptsFlag = "--ignore-scripts" + npmNoAuditFlag = "--no-audit" + npmNoFundFlag = "--no-fund" + + configIgnoreScriptsEnv = "NPM_CONFIG_IGNORE_SCRIPTS" + configAuditEnv = "NPM_CONFIG_AUDIT" + configFundEnv = "NPM_CONFIG_FUND" + configLevelEnv = "NPM_CONFIG_LOGLEVEL" + ciEnv = "CI" + noUpdateNotifierEnv = "NO_UPDATE_NOTIFIER" + + npmDescriptorFile = "package.json" + npmInstallTimeout = 15 * time.Minute + + // 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*")[~^]?[^"]+(")` +) + +var npmInstallEnvVars = map[string]string{ + configIgnoreScriptsEnv: "true", + configAuditEnv: "false", + configFundEnv: "false", + configLevelEnv: "error", + ciEnv: "true", + noUpdateNotifierEnv: "1", +} + +type NpmPackageHandler struct { + CommonPackageHandler +} + +// TODO eran check manually connection to Artifactory with self defined .npmrc and that it is not being override by our env vars + +func (npm *NpmPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { + if vulnDetails.IsDirectDependency { + return npm.updateDirectDependency(vulnDetails) + } + return &utils.ErrUnsupportedFix{ + PackageName: vulnDetails.ImpactedDependencyName, + FixedVersion: vulnDetails.SuggestedFixedVersion, + ErrorType: utils.IndirectDependencyFixNotSupported, + } +} + +func (npm *NpmPackageHandler) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) error { + /* + // todo eran remove this assignment + vulnDetails.ImpactPaths[0][1].Location = &formats.Location{ + File: "package-lock.json", + } + + */ + descriptorPaths, err := npm.getDescriptorsToFixFromVulnerability(vulnDetails) + if err != nil { + return err + } + + originalWd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + + vulnRegexp := GetVulnerabilityRegexCompiler(vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, npmDependencyRegexpPattern) + + for _, descriptorPath := range descriptorPaths { + if fixErr := npm.fixVulnerabilityInDescriptor(vulnDetails, descriptorPath, originalWd, vulnRegexp); fixErr != nil { + err = errors.Join(err, fixErr) + } + } + if err != nil { + return fmt.Errorf("failed to fix vulnerability in one of the following descriptors [%s]: %w", strings.Join(descriptorPaths, ", "), err) + } + + return nil +} + +// Returns all descriptors related to the vulnerability based on its lock file locations +func (npm *NpmPackageHandler) getDescriptorsToFixFromVulnerability(vulnDetails *utils.VulnerabilityDetails) ([]string, error) { + lockFilePaths := GetVulnerabilityLocations(vulnDetails) + if len(lockFilePaths) == 0 { + return nil, fmt.Errorf("no location evidence was found for package %s", vulnDetails.ImpactedDependencyName) + } + + var descriptorPaths []string + for _, lockFilePath := range lockFilePaths { + // We currently assume the descriptor resides in the same directory as the lock file, and this is the only supported use case + descriptorPath := filepath.Join(filepath.Dir(lockFilePath), npmDescriptorFile) + fileExists, err := fileutils.IsFileExists(descriptorPath, false) + if err != nil { + return nil, err + } + if !fileExists { + return nil, fmt.Errorf("descriptor file '%s' not found for lock file '%s': %w", descriptorPath, lockFilePath, err) + } + descriptorPaths = append(descriptorPaths, descriptorPath) + } + return descriptorPaths, nil +} + +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) + } + + if !vulnRegexp.MatchString(strings.ToLower(string(descriptorContent))) { + return fmt.Errorf("dependency '%s' with version '%s' not found in descriptor '%s' despite lock file evidence", vulnDetails.ImpactedDependencyName, vulnDetails.ImpactedDependencyVersion, descriptorPath) + } + + backupContent := descriptorContent + updatedContent, err := npm.updateVersionInDescriptor(descriptorContent, vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion) + if err != nil { + return fmt.Errorf("failed to update version in descriptor: %w", err) + } + + if err = os.WriteFile(descriptorPath, updatedContent, 0644); err != nil { + return fmt.Errorf("failed to write updated descriptor '%s': %w", descriptorPath, err) + } + + // 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 +} + +func (npm *NpmPackageHandler) updateVersionInDescriptor(content []byte, packageName, newVersion string) ([]byte, error) { + escapedName := regexp.QuoteMeta(packageName) + replacePattern := fmt.Sprintf(npmDependencyReplacePattern, escapedName) + replaceRegex := regexp.MustCompile("(?i)" + replacePattern) + + replacement := fmt.Sprintf("${1}%s${2}", newVersion) + updatedContent := replaceRegex.ReplaceAll(content, []byte(replacement)) + + if string(content) == string(updatedContent) { + return nil, fmt.Errorf("failed to find and replace version for package '%s'", packageName) + } + return updatedContent, nil +} + +func (npm *NpmPackageHandler) regenerateLockFileWithRetry() error { + err := npm.runNpmInstall() + if err == nil { + return 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) + } + return nil +} + +func (npm *NpmPackageHandler) runNpmInstall() error { + args := []string{ + "install", + npmPackageLockOnlyFlag, + npmIgnoreScriptsFlag, + npmNoAuditFlag, + npmNoFundFlag, + } + + fullCommand := "npm " + strings.Join(args, " ") + log.Debug(fmt.Sprintf("Running '%s'", fullCommand)) + + ctx, cancel := context.WithTimeout(context.Background(), npmInstallTimeout) + defer cancel() + + //#nosec G204 -- False positive - the subprocess only runs after the user's approval + cmd := exec.CommandContext(ctx, "npm", args...) + + cmd.Env = npm.buildIsolatedEnv() + output, err := cmd.CombinedOutput() + + // TODO eran check manually that timeout is working by setting very low timeout + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return fmt.Errorf("npm install timed out after %v", npmInstallTimeout) + } + + if err != nil { + return fmt.Errorf("npm install failed: %w\nOutput: %s", err, string(output)) + } + + return nil +} + +// Creates an environment slice with npm isolation variables that override user's .npmrc settings for specific options while allowing registry configuration to pass through +func (npm *NpmPackageHandler) buildIsolatedEnv() []string { + env := os.Environ() + for key, value := range npmInstallEnvVars { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + + return env +} From 1b56a43858085a2199c2833674632873e52f5144 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 7 Jan 2026 11:46:36 +0200 Subject: [PATCH 2/8] improving GetVulnerabilityRegexCompiler and adding extractor for file location extraction for a given VulnerabilityDetails --- packagehandlers/commonpackagehandler.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packagehandlers/commonpackagehandler.go b/packagehandlers/commonpackagehandler.go index 887a2a210..d0eddc09b 100644 --- a/packagehandlers/commonpackagehandler.go +++ b/packagehandlers/commonpackagehandler.go @@ -8,11 +8,11 @@ import ( "regexp" "strings" + "github.com/jfrog/frogbot/v2/utils" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/utils/log" - - "github.com/jfrog/frogbot/v2/utils" ) // PackageHandler interface to hold operations on packages @@ -136,10 +136,19 @@ func (cph *CommonPackageHandler) GetAllDescriptorFilesFullPaths(descriptorFilesS // 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. // For example, in Gradle descriptors, a dependency line may consist of two components for the dependency's name (e.g., implementation group: 'junit', name: 'junit', version: '4.7'), therefore this func cannot be utilized in this case. func GetVulnerabilityRegexCompiler(impactedName, impactedVersion, dependencyLineFormat string) *regexp.Regexp { - // We replace '.' with '\\.' since '.' is a special character in regexp patterns, and we want to capture the character '.' itself - // To avoid dealing with case sensitivity we lower all characters in the package's name and in the file we check - regexpFitImpactedName := strings.ToLower(strings.ReplaceAll(impactedName, ".", "\\.")) - regexpFitImpactedVersion := strings.ToLower(strings.ReplaceAll(impactedVersion, ".", "\\.")) + regexpFitImpactedName := strings.ToLower(regexp.QuoteMeta(impactedName)) + regexpFitImpactedVersion := strings.ToLower(regexp.QuoteMeta(impactedVersion)) regexpCompleteFormat := fmt.Sprintf(strings.ToLower(dependencyLineFormat), regexpFitImpactedName, regexpFitImpactedVersion) return regexp.MustCompile(regexpCompleteFormat) } + +// Extracts unique file paths from the vulnerability's component locations. +func GetVulnerabilityLocations(vulnDetails *utils.VulnerabilityDetails) []string { + pathsSet := datastructures.MakeSet[string]() + for _, component := range vulnDetails.Components { + if component.Location != nil && component.Location.File != "" { + pathsSet.Add(component.Location.File) + } + } + return pathsSet.ToSlice() +} From 23f38c879750fa1178d9d66e4316181853fefab8 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 7 Jan 2026 11:47:02 +0200 Subject: [PATCH 3/8] renamed file to better adjust new package refactoring and naming --- ...s_test.go => commonpackageupdater_test.go} | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) rename packagehandlers/{packagehandlers_test.go => commonpackageupdater_test.go} (92%) diff --git a/packagehandlers/packagehandlers_test.go b/packagehandlers/commonpackageupdater_test.go similarity index 92% rename from packagehandlers/packagehandlers_test.go rename to packagehandlers/commonpackageupdater_test.go index c50aacac4..4531c31b8 100644 --- a/packagehandlers/packagehandlers_test.go +++ b/packagehandlers/commonpackageupdater_test.go @@ -1017,3 +1017,97 @@ func TestPnpmFixVulnerabilityIfExists(t *testing.T) { assert.NoError(t, err) assert.False(t, nodeModulesExist) } + +func TestGetVulnerabilityLocations(t *testing.T) { + testcases := []struct { + name string + vulnDetails *utils.VulnerabilityDetails + expectedPaths []string + }{ + { + name: "single component with location", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: "/repo/package-lock.json"}}, + }, + }, + }, + }, + expectedPaths: []string{"/repo/package-lock.json"}, + }, + { + name: "multiple components with same location - deduplicated", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: "/repo/package-lock.json"}}, + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: "/repo/package-lock.json"}}, + }, + }, + }, + }, + expectedPaths: []string{"/repo/package-lock.json"}, + }, + { + name: "multiple components with different locations", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: "/repo/app1/package-lock.json"}}, + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: "/repo/app2/package-lock.json"}}, + }, + }, + }, + }, + expectedPaths: []string{"/repo/app1/package-lock.json", "/repo/app2/package-lock.json"}, + }, + { + name: "component with nil location", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: nil}, + }, + }, + }, + }, + expectedPaths: []string{}, + }, + { + name: "component with empty file path", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: ""}}, + }, + }, + }, + }, + expectedPaths: []string{}, + }, + { + name: "no components", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + Components: []formats.ComponentRow{}, + }, + }, + }, + expectedPaths: []string{}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result := GetVulnerabilityLocations(tc.vulnDetails) + assert.ElementsMatch(t, tc.expectedPaths, result) + }) + } +} From 5c3e233526e970543f5a9e6388aac14795805066 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 7 Jan 2026 11:52:30 +0200 Subject: [PATCH 4/8] adding tests for new functions --- packagehandlers/npmpackageupdater_test.go | 297 ++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 packagehandlers/npmpackageupdater_test.go diff --git a/packagehandlers/npmpackageupdater_test.go b/packagehandlers/npmpackageupdater_test.go new file mode 100644 index 000000000..b2e3f5169 --- /dev/null +++ b/packagehandlers/npmpackageupdater_test.go @@ -0,0 +1,297 @@ +package packagehandlers + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/jfrog/frogbot/v2/utils" + "github.com/jfrog/jfrog-cli-security/utils/formats" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/stretchr/testify/assert" +) + +func TestNpmGetVulnerabilityRegexCompiler(t *testing.T) { + testcases := []struct { + name string + packageName string + version string + testContent string + shouldMatch bool + }{ + { + name: "exact version match", + packageName: "minimist", + version: "1.2.5", + testContent: `"minimist": "1.2.5"`, + shouldMatch: true, + }, + { + name: "version with caret prefix", + packageName: "lodash", + version: "4.17.0", + testContent: `"lodash": "^4.17.0"`, + shouldMatch: true, + }, + { + name: "version with tilde prefix", + packageName: "express", + version: "4.18.0", + testContent: `"express": "~4.18.0"`, + shouldMatch: true, + }, + { + name: "scoped package", + packageName: "@types/node", + version: "18.0.0", + testContent: `"@types/node": "18.0.0"`, + shouldMatch: true, + }, + { + name: "version mismatch", + packageName: "minimist", + version: "1.2.5", + testContent: `"minimist": "1.2.6"`, + shouldMatch: false, + }, + { + name: "package name mismatch", + packageName: "minimist", + version: "1.2.5", + testContent: `"minimatch": "1.2.5"`, + shouldMatch: false, + }, + { + name: "case insensitive package name", + packageName: "Minimist", + version: "1.2.5", + testContent: `"minimist": "1.2.5"`, + shouldMatch: true, + }, + { + name: "version with plus sign (build metadata)", + packageName: "somepackage", + version: "1.0.0+build.123", + testContent: `"somepackage": "1.0.0+build.123"`, + shouldMatch: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + regex := GetVulnerabilityRegexCompiler(tc.packageName, tc.version, npmDependencyRegexpPattern) + matches := regex.MatchString(strings.ToLower(tc.testContent)) + assert.Equal(t, tc.shouldMatch, matches, "Pattern: %s, Content: %s", regex.String(), tc.testContent) + }) + } +} + +func TestUpdateVersionInDescriptor(t *testing.T) { + npm := &NpmPackageHandler{} + + testcases := []struct { + name string + originalContent string + packageName string + newVersion string + expectedContent string + expectError bool + }{ + { + name: "update exact version", + originalContent: `{"dependencies": {"minimist": "1.2.5"}}`, + packageName: "minimist", + newVersion: "1.2.6", + expectedContent: `{"dependencies": {"minimist": "1.2.6"}}`, + expectError: false, + }, + { + name: "update version with caret prefix - removes prefix", + originalContent: `{"dependencies": {"lodash": "^4.17.0"}}`, + packageName: "lodash", + newVersion: "4.17.21", + expectedContent: `{"dependencies": {"lodash": "4.17.21"}}`, + expectError: false, + }, + { + name: "update version with tilde prefix - removes prefix", + originalContent: `{"dependencies": {"express": "~4.18.0"}}`, + packageName: "express", + newVersion: "4.18.2", + expectedContent: `{"dependencies": {"express": "4.18.2"}}`, + expectError: false, + }, + { + name: "update scoped package", + originalContent: `{"dependencies": {"@types/node": "18.0.0"}}`, + packageName: "@types/node", + newVersion: "18.11.0", + expectedContent: `{"dependencies": {"@types/node": "18.11.0"}}`, + expectError: false, + }, + { + name: "package not found", + originalContent: `{"dependencies": {"lodash": "4.17.0"}}`, + packageName: "minimist", + newVersion: "1.2.6", + expectedContent: "", + expectError: true, + }, + { + name: "preserve formatting with spaces", + originalContent: `{ + "dependencies": { + "minimist": "1.2.5" + } +}`, + packageName: "minimist", + newVersion: "1.2.6", + expectedContent: `{ + "dependencies": { + "minimist": "1.2.6" + } +}`, + expectError: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result, err := npm.updateVersionInDescriptor([]byte(tc.originalContent), tc.packageName, tc.newVersion) + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedContent, string(result)) + } + }) + } +} + +func TestBuildIsolatedEnv(t *testing.T) { + npm := &NpmPackageHandler{} + env := npm.buildIsolatedEnv() + + // Convert to map for easier checking + envMap := make(map[string]string) + for _, e := range env { + parts := strings.SplitN(e, "=", 2) + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + + // Verify all required env vars are set + assert.Equal(t, "true", envMap[configIgnoreScriptsEnv]) + assert.Equal(t, "false", envMap[configAuditEnv]) + assert.Equal(t, "false", envMap[configFundEnv]) + assert.Equal(t, "error", envMap[configLevelEnv]) + assert.Equal(t, "true", envMap[ciEnv]) + assert.Equal(t, "1", envMap[noUpdateNotifierEnv]) +} + +func TestNpmGetDescriptorPathsFromVulnerability(t *testing.T) { + npm := &NpmPackageHandler{} + tmpDir, err := os.MkdirTemp("", "npm-descriptor-test-") + assert.NoError(t, err) + defer func() { + assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) + }() + + // Create a package.json in the temp directory + packageJsonPath := filepath.Join(tmpDir, "package.json") + assert.NoError(t, os.WriteFile(packageJsonPath, []byte(`{"name": "test"}`), 0644)) + + // Create nested directory with package.json + nestedDir := filepath.Join(tmpDir, "apps", "frontend") + assert.NoError(t, os.MkdirAll(nestedDir, 0755)) + nestedPackageJsonPath := filepath.Join(nestedDir, "package.json") + assert.NoError(t, os.WriteFile(nestedPackageJsonPath, []byte(`{"name": "frontend"}`), 0644)) + + testcases := []struct { + name string + vulnDetails *utils.VulnerabilityDetails + expectedPaths []string + expectError bool + errorContains string + }{ + { + name: "derives package.json from package-lock.json path", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "minimist", + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: filepath.Join(tmpDir, "package-lock.json")}}, + }, + }, + }, + }, + expectedPaths: []string{packageJsonPath}, + expectError: false, + }, + { + name: "derives package.json from nested directory", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "minimist", + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: filepath.Join(nestedDir, "package-lock.json")}}, + }, + }, + }, + }, + expectedPaths: []string{nestedPackageJsonPath}, + expectError: false, + }, + { + name: "error when no location evidence found", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "minimist", + Components: []formats.ComponentRow{}, + }, + }, + }, + expectedPaths: nil, + expectError: true, + errorContains: "no location evidence was found", + }, + { + name: "error when descriptor file does not exist", + vulnDetails: &utils.VulnerabilityDetails{ + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: "minimist", + Components: []formats.ComponentRow{ + {Name: "minimist", Version: "1.2.5", Location: &formats.Location{File: "/nonexistent/path/package-lock.json"}}, + }, + }, + }, + }, + expectedPaths: nil, + expectError: true, + errorContains: "not found for lock file", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + result, err := npm.getDescriptorsToFixFromVulnerability(tc.vulnDetails) + if tc.expectError { + assert.Error(t, err) + if tc.errorContains != "" { + assert.Contains(t, err.Error(), tc.errorContains) + } + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.ElementsMatch(t, tc.expectedPaths, result) + } + }) + } +} From 0d44549974e9fbcc25ceeb1362ac86fc49eb9cb0 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 7 Jan 2026 15:48:02 +0200 Subject: [PATCH 5/8] fix UpdateDependency test for NPM --- packagehandlers/commonpackageupdater_test.go | 69 +++++++++++++++----- testdata/projects/npm/package-lock.json | 22 +++++++ 2 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 testdata/projects/npm/package-lock.json diff --git a/packagehandlers/commonpackageupdater_test.go b/packagehandlers/commonpackageupdater_test.go index 4531c31b8..ba7386520 100644 --- a/packagehandlers/commonpackageupdater_test.go +++ b/packagehandlers/commonpackageupdater_test.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "regexp" - "strconv" "strings" "testing" @@ -23,9 +22,11 @@ type dependencyFixTest struct { vulnDetails *utils.VulnerabilityDetails scanDetails *utils.ScanDetails fixSupported bool + errorExpected bool specificTechVersion string testDirName string descriptorsToCheck []string + testcaseInfo string } const ( @@ -153,25 +154,25 @@ func TestUpdateDependency(t *testing.T) { // Npm test cases { { - // This test case is designed to use a project that doesn't exist in the testdata/indirect-projects directory. Its purpose is to confirm that we correctly skip fixing an indirect dependency. - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "0.8.4", - IsDirectDependency: false, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{Technology: techutils.Npm, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "mpath"}}, - }, + // Test project doesn't exist for the testcase - we just check skipping indirect dependency fix + testcaseInfo: "test-skip-fixing-indirect", + vulnDetails: createVulnerabilityDetails(techutils.Npm, "mpath", "0.8.3", "0.8.4", false, "package-lock.json"), scanDetails: scanDetails, fixSupported: false, }, { - vulnDetails: &utils.VulnerabilityDetails{ - SuggestedFixedVersion: "1.2.6", - IsDirectDependency: true, - VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{Technology: techutils.Npm, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "minimist"}}, - }, + vulnDetails: createVulnerabilityDetails(techutils.Npm, "minimist", "1.2.5", "1.2.6", true, "package-lock.json"), scanDetails: scanDetails, fixSupported: true, descriptorsToCheck: []string{"package.json"}, }, + { + testcaseInfo: "no-location-evidence", + vulnDetails: createVulnerabilityDetails(techutils.Npm, "minimist", "1.2.5", "1.2.6", true, ""), + scanDetails: scanDetails, + fixSupported: true, + errorExpected: true, + }, }, // Yarn test cases @@ -363,7 +364,7 @@ func TestUpdateDependency(t *testing.T) { for _, testBatch := range testCases { for _, test := range testBatch { packageHandler := GetCompatiblePackageHandler(test.vulnDetails, test.scanDetails) - t.Run(fmt.Sprintf("%s:%s direct:%s", test.vulnDetails.Technology.String()+test.specificTechVersion, test.vulnDetails.ImpactedDependencyName, strconv.FormatBool(test.vulnDetails.IsDirectDependency)), + t.Run(getUpdateDependencyTestcaseName(test.vulnDetails.Technology.String()+test.specificTechVersion, test.vulnDetails.IsDirectDependency, test.testcaseInfo), func(t *testing.T) { testDataDir := getTestDataDir(t, test.vulnDetails.IsDirectDependency) testDirName := test.vulnDetails.Technology.String() @@ -374,8 +375,12 @@ func TestUpdateDependency(t *testing.T) { defer cleanup() err := packageHandler.UpdateDependency(test.vulnDetails) if test.fixSupported { - assert.NoError(t, err) - verifyDependencyUpdate(t, test) + if test.errorExpected { + assert.Error(t, err) + } else { + assert.NoError(t, err) + verifyDependencyUpdate(t, test) + } } else { assert.Error(t, err) assert.IsType(t, &utils.ErrUnsupportedFix{}, err, "Expected unsupported fix error") @@ -1111,3 +1116,37 @@ func TestGetVulnerabilityLocations(t *testing.T) { }) } } + +func getUpdateDependencyTestcaseName(technology string, isDirect bool, extraTestInfo string) string { + testName := technology + if isDirect { + testName += "-direct-dep" + } else { + testName += "-indirect-dep" + } + if extraTestInfo != "" { + testName += "_(" + extraTestInfo + ")" + } + return testName +} + +func createVulnerabilityDetails(technology techutils.Technology, packageName, packageVersion, fixedVersion string, isDirectDependency bool, locationEvidencePath string) *utils.VulnerabilityDetails { + return &utils.VulnerabilityDetails{ + SuggestedFixedVersion: fixedVersion, + IsDirectDependency: isDirectDependency, + VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{ + Technology: technology, + ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ + ImpactedDependencyName: packageName, + ImpactedDependencyVersion: packageVersion, + Components: []formats.ComponentRow{ + { + Name: packageName, + Version: packageVersion, + Location: &formats.Location{File: locationEvidencePath}, + }, + }, + }, + }, + } +} diff --git a/testdata/projects/npm/package-lock.json b/testdata/projects/npm/package-lock.json new file mode 100644 index 000000000..dfb2a025a --- /dev/null +++ b/testdata/projects/npm/package-lock.json @@ -0,0 +1,22 @@ +{ + "name": "npm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "npm", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "minimist": "1.2.5" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://entplus.jfrog.io/artifactory/api/npm/npm-virtual/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "license": "MIT" + } + } +} From 5e027cc1234b056c58a7762fda892bff2a7aec62 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Wed, 7 Jan 2026 15:55:45 +0200 Subject: [PATCH 6/8] remove comments --- packagehandlers/npmpackageupdater.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packagehandlers/npmpackageupdater.go b/packagehandlers/npmpackageupdater.go index 7ebff692b..908ac28e7 100644 --- a/packagehandlers/npmpackageupdater.go +++ b/packagehandlers/npmpackageupdater.go @@ -51,8 +51,6 @@ type NpmPackageHandler struct { CommonPackageHandler } -// TODO eran check manually connection to Artifactory with self defined .npmrc and that it is not being override by our env vars - func (npm *NpmPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails) error { if vulnDetails.IsDirectDependency { return npm.updateDirectDependency(vulnDetails) @@ -65,13 +63,6 @@ func (npm *NpmPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityD } func (npm *NpmPackageHandler) updateDirectDependency(vulnDetails *utils.VulnerabilityDetails) error { - /* - // todo eran remove this assignment - vulnDetails.ImpactPaths[0][1].Location = &formats.Location{ - File: "package-lock.json", - } - - */ descriptorPaths, err := npm.getDescriptorsToFixFromVulnerability(vulnDetails) if err != nil { return err @@ -210,7 +201,6 @@ func (npm *NpmPackageHandler) runNpmInstall() error { cmd.Env = npm.buildIsolatedEnv() output, err := cmd.CombinedOutput() - // TODO eran check manually that timeout is working by setting very low timeout if errors.Is(ctx.Err(), context.DeadlineExceeded) { return fmt.Errorf("npm install timed out after %v", npmInstallTimeout) } From 28504c1f5d32f55f480fb8155cfde3ed1b0f47c1 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Thu, 8 Jan 2026 09:57:09 +0200 Subject: [PATCH 7/8] improve tests --- ...kagehandler.go => commonpackageupdater.go} | 0 packagehandlers/commonpackageupdater_test.go | 375 +++++++++++++++++- 2 files changed, 363 insertions(+), 12 deletions(-) rename packagehandlers/{commonpackagehandler.go => commonpackageupdater.go} (100%) diff --git a/packagehandlers/commonpackagehandler.go b/packagehandlers/commonpackageupdater.go similarity index 100% rename from packagehandlers/commonpackagehandler.go rename to packagehandlers/commonpackageupdater.go diff --git a/packagehandlers/commonpackageupdater_test.go b/packagehandlers/commonpackageupdater_test.go index ba7386520..eaedd082c 100644 --- a/packagehandlers/commonpackageupdater_test.go +++ b/packagehandlers/commonpackageupdater_test.go @@ -27,6 +27,8 @@ type dependencyFixTest struct { testDirName string descriptorsToCheck []string testcaseInfo string + // For this param give the relative path from the test project root + lockFileToVerifyItsChange string } const ( @@ -161,10 +163,11 @@ func TestUpdateDependency(t *testing.T) { fixSupported: false, }, { - vulnDetails: createVulnerabilityDetails(techutils.Npm, "minimist", "1.2.5", "1.2.6", true, "package-lock.json"), - scanDetails: scanDetails, - fixSupported: true, - descriptorsToCheck: []string{"package.json"}, + vulnDetails: createVulnerabilityDetails(techutils.Npm, "minimist", "1.2.5", "1.2.6", true, "package-lock.json"), + scanDetails: scanDetails, + fixSupported: true, + descriptorsToCheck: []string{"package.json"}, + lockFileToVerifyItsChange: "package-lock.json", }, { testcaseInfo: "no-location-evidence", @@ -373,17 +376,31 @@ func TestUpdateDependency(t *testing.T) { } cleanup := createTempDirAndChdir(t, testDataDir, testDirName+test.specificTechVersion) defer cleanup() + + var lockFileContentBeforeUpdate []byte + if test.lockFileToVerifyItsChange != "" { + var readErr error + lockFileContentBeforeUpdate, readErr = os.ReadFile(test.lockFileToVerifyItsChange) + assert.NoError(t, readErr, "Failed to read lock file before update") + } + err := packageHandler.UpdateDependency(test.vulnDetails) - if test.fixSupported { - if test.errorExpected { - assert.Error(t, err) - } else { - assert.NoError(t, err) - verifyDependencyUpdate(t, test) - } - } else { + if !test.fixSupported { assert.Error(t, err) assert.IsType(t, &utils.ErrUnsupportedFix{}, err, "Expected unsupported fix error") + return + } + if test.errorExpected { + assert.Error(t, err) + return + } + assert.NoError(t, err) + verifyDependencyUpdate(t, test) + + if test.lockFileToVerifyItsChange != "" { + lockFileContentAfter, readErr := os.ReadFile(test.lockFileToVerifyItsChange) + assert.NoError(t, readErr, "Failed to read lock file after update") + assert.NotEqual(t, lockFileContentBeforeUpdate, lockFileContentAfter, "Lock file should have been updated") } }) } @@ -1130,6 +1147,340 @@ func getUpdateDependencyTestcaseName(technology string, isDirect bool, extraTest return testName } +func TestGetVulnerabilityRegexCompiler(t *testing.T) { + // Sample format patterns from different package managers + const ( + npmPattern = `\s*"%s"\s*:\s*"[~^]?%s"` + dotnetPattern = "include=[\\\"|\\']%s[\\\"|\\']\\s*version=[\\\"|\\']%s[\\\"|\\']" + simplePattern = `%s:%s` + ) + + testcases := []struct { + name string + packageName string + packageVer string + formatPattern string + testContent string + shouldMatch bool + }{ + // Basic matching + { + name: "basic npm match", + packageName: "lodash", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: `"lodash": "4.17.20"`, + shouldMatch: true, + }, + { + name: "npm with caret prefix", + packageName: "lodash", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: `"lodash": "^4.17.20"`, + shouldMatch: true, + }, + { + name: "npm with tilde prefix", + packageName: "lodash", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: `"lodash": "~4.17.20"`, + shouldMatch: true, + }, + { + name: "npm version mismatch", + packageName: "lodash", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: `"lodash": "4.17.21"`, + shouldMatch: false, + }, + { + name: "npm name mismatch", + packageName: "lodash", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: `"underscore": "4.17.20"`, + shouldMatch: false, + }, + + // Case insensitivity + { + name: "case insensitive package name", + packageName: "PyJWT", + packageVer: "2.4.0", + formatPattern: simplePattern, + testContent: `pyjwt:2.4.0`, + shouldMatch: true, + }, + { + name: "case insensitive mixed case", + packageName: "LODASH", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: `"lodash": "4.17.20"`, + shouldMatch: true, + }, + + // Scoped npm packages with @ + { + name: "scoped npm package", + packageName: "@types/node", + packageVer: "18.0.0", + formatPattern: npmPattern, + testContent: `"@types/node": "18.0.0"`, + shouldMatch: true, + }, + { + name: "scoped package with org", + packageName: "@angular/core", + packageVer: "15.0.0", + formatPattern: npmPattern, + testContent: `"@angular/core": "^15.0.0"`, + shouldMatch: true, + }, + + // Regex special characters in package name - should be escaped + { + name: "package name with dot", + packageName: "lodash.merge", + packageVer: "4.6.2", + formatPattern: npmPattern, + testContent: `"lodash.merge": "4.6.2"`, + shouldMatch: true, + }, + { + name: "dot should not match any character", + packageName: "lodash.merge", + packageVer: "4.6.2", + formatPattern: npmPattern, + testContent: `"lodashXmerge": "4.6.2"`, + shouldMatch: false, + }, + { + name: "package name with asterisk", + packageName: "test*package", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test*package:1.0.0`, + shouldMatch: true, + }, + { + name: "asterisk should not match multiple chars", + packageName: "test*package", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `testABCpackage:1.0.0`, + shouldMatch: false, + }, + { + name: "package name with question mark", + packageName: "test?pkg", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test?pkg:1.0.0`, + shouldMatch: true, + }, + { + name: "question mark should not match single char", + packageName: "test?pkg", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `testXpkg:1.0.0`, + shouldMatch: false, + }, + { + name: "package name with brackets", + packageName: "test[pkg]", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test[pkg]:1.0.0`, + shouldMatch: true, + }, + { + name: "package name with parentheses", + packageName: "test(pkg)", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test(pkg):1.0.0`, + shouldMatch: true, + }, + { + name: "package name with curly braces", + packageName: "test{pkg}", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test{pkg}:1.0.0`, + shouldMatch: true, + }, + { + name: "package name with pipe", + packageName: "test|pkg", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test|pkg:1.0.0`, + shouldMatch: true, + }, + { + name: "pipe should not match as OR", + packageName: "test|pkg", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test:1.0.0`, + shouldMatch: false, + }, + { + name: "package name with caret and dollar", + packageName: "^test$", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `^test$:1.0.0`, + shouldMatch: true, + }, + { + name: "package name with backslash", + packageName: `test\pkg`, + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `test\pkg:1.0.0`, + shouldMatch: true, + }, + + // Version with special characters + { + name: "version with plus (build metadata)", + packageName: "mypackage", + packageVer: "1.0.0+build123", + formatPattern: simplePattern, + testContent: `mypackage:1.0.0+build123`, + shouldMatch: true, + }, + { + name: "plus in version should not match one-or-more", + packageName: "mypackage", + packageVer: "1.0.0+", + formatPattern: simplePattern, + testContent: `mypackage:1.0.00000`, + shouldMatch: false, + }, + { + name: "version with dots should match literally", + packageName: "pkg", + packageVer: "1.2.3", + formatPattern: simplePattern, + testContent: `pkg:1.2.3`, + shouldMatch: true, + }, + { + name: "dots should not match any char", + packageName: "pkg", + packageVer: "1.2.3", + formatPattern: simplePattern, + testContent: `pkg:1X2Y3`, + shouldMatch: false, + }, + + // Empty name and version edge cases + { + name: "empty package name matches empty", + packageName: "", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `:1.0.0`, + shouldMatch: true, + }, + { + name: "empty version matches empty", + packageName: "pkg", + packageVer: "", + formatPattern: simplePattern, + testContent: `pkg:`, + shouldMatch: true, + }, + { + name: "both empty", + packageName: "", + packageVer: "", + formatPattern: simplePattern, + testContent: `:`, + shouldMatch: true, + }, + + // Complex realistic scenarios + { + name: "dotnet pattern match", + packageName: "Newtonsoft.Json", + packageVer: "13.0.1", + formatPattern: dotnetPattern, + testContent: `Include="Newtonsoft.Json" Version="13.0.1"`, + shouldMatch: true, + }, + { + name: "dotnet single quotes", + packageName: "Newtonsoft.Json", + packageVer: "13.0.1", + formatPattern: dotnetPattern, + testContent: `Include='Newtonsoft.Json' Version='13.0.1'`, + shouldMatch: true, + }, + + // Whitespace handling + { + name: "npm with extra whitespace", + packageName: "lodash", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: ` "lodash" : "4.17.20"`, + shouldMatch: true, + }, + { + name: "npm with tabs", + packageName: "lodash", + packageVer: "4.17.20", + formatPattern: npmPattern, + testContent: "\t\"lodash\"\t:\t\"4.17.20\"", + shouldMatch: true, + }, + + // Unicode characters (less common but possible) + { + name: "package name with unicode", + packageName: "пакет", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `пакет:1.0.0`, + shouldMatch: true, + }, + + // Long package names and versions + { + name: "very long package name", + packageName: "this-is-a-very-long-package-name-that-might-exist-in-real-world", + packageVer: "1.0.0", + formatPattern: simplePattern, + testContent: `this-is-a-very-long-package-name-that-might-exist-in-real-world:1.0.0`, + shouldMatch: true, + }, + { + name: "prerelease version", + packageName: "pkg", + packageVer: "1.0.0-alpha.1", + formatPattern: simplePattern, + testContent: `pkg:1.0.0-alpha.1`, + shouldMatch: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + regex := GetVulnerabilityRegexCompiler(tc.packageName, tc.packageVer, tc.formatPattern) + matches := regex.MatchString(strings.ToLower(tc.testContent)) + assert.Equal(t, tc.shouldMatch, matches, "Pattern: %s, Content: %s", regex.String(), tc.testContent) + }) + } +} + func createVulnerabilityDetails(technology techutils.Technology, packageName, packageVersion, fixedVersion string, isDirectDependency bool, locationEvidencePath string) *utils.VulnerabilityDetails { return &utils.VulnerabilityDetails{ SuggestedFixedVersion: fixedVersion, From 888f5a96b7d6e0f6c632bb4bc571393ece429f73 Mon Sep 17 00:00:00 2001 From: Eran Turgeman Date: Thu, 8 Jan 2026 09:58:47 +0200 Subject: [PATCH 8/8] / --- packagehandlers/commonpackageupdater_test.go | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packagehandlers/commonpackageupdater_test.go b/packagehandlers/commonpackageupdater_test.go index eaedd082c..8324ededf 100644 --- a/packagehandlers/commonpackageupdater_test.go +++ b/packagehandlers/commonpackageupdater_test.go @@ -1134,19 +1134,6 @@ func TestGetVulnerabilityLocations(t *testing.T) { } } -func getUpdateDependencyTestcaseName(technology string, isDirect bool, extraTestInfo string) string { - testName := technology - if isDirect { - testName += "-direct-dep" - } else { - testName += "-indirect-dep" - } - if extraTestInfo != "" { - testName += "_(" + extraTestInfo + ")" - } - return testName -} - func TestGetVulnerabilityRegexCompiler(t *testing.T) { // Sample format patterns from different package managers const ( @@ -1481,6 +1468,19 @@ func TestGetVulnerabilityRegexCompiler(t *testing.T) { } } +func getUpdateDependencyTestcaseName(technology string, isDirect bool, extraTestInfo string) string { + testName := technology + if isDirect { + testName += "-direct-dep" + } else { + testName += "-indirect-dep" + } + if extraTestInfo != "" { + testName += "_(" + extraTestInfo + ")" + } + return testName +} + func createVulnerabilityDetails(technology techutils.Technology, packageName, packageVersion, fixedVersion string, isDirectDependency bool, locationEvidencePath string) *utils.VulnerabilityDetails { return &utils.VulnerabilityDetails{ SuggestedFixedVersion: fixedVersion,