Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ require (
github.com/go-git/go-git/v5 v5.16.3
github.com/golang/mock v1.6.0
github.com/google/go-github/v45 v45.2.0
github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9
github.com/jfrog/build-info-go v1.13.1-0.20260120103048-d7f367bfa36e
github.com/jfrog/froggit-go v1.20.6
github.com/jfrog/gofrog v1.7.6
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5
github.com/jfrog/jfrog-cli-security v1.24.2
github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260120063955-c654c159290e
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260112010739-87fc7275623c
github.com/jfrog/jfrog-cli-security v1.26.0
github.com/jfrog/jfrog-client-go v1.55.1-0.20260120055025-12f25e12798a
github.com/owenrumney/go-sarif/v3 v3.2.3
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
Expand Down
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,22 @@ github.com/jedib0t/go-pretty/v6 v6.7.5 h1:9dJSWTJnsXJVVAbvxIFxeHf/JxoJd7GUl5o3Uz
github.com/jedib0t/go-pretty/v6 v6.7.5/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI=
github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw=
github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9 h1:CL7lp7Y7srwQ1vy1btX66t4wbztzEGQbqi/9tdEz7xk=
github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9/go.mod h1:9W4U440fdTHwW1HiB/R0VQvz/5q8ZHsms9MWcq+JrdY=
github.com/jfrog/build-info-go v1.13.1-0.20260120103048-d7f367bfa36e h1:STiWjuLtlEFR1H3kSKw6vDGhGdtUmV6O+ljPfrQ14sI=
github.com/jfrog/build-info-go v1.13.1-0.20260120103048-d7f367bfa36e/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo=
github.com/jfrog/froggit-go v1.20.6 h1:Xp7+LlEh0m1KGrQstb+u0aGfjRUtv1eh9xQBV3571jQ=
github.com/jfrog/froggit-go v1.20.6/go.mod h1:obSG1SlsWjktkuqmKtpq7MNTTL63e0ot+ucTnlOMV88=
github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s=
github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4=
github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY=
github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93 h1:rpkJZN0TigpAGY/bfgmLO4nwhyhkr0gkBTLz/0B5zS8=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93/go.mod h1:7cCaRhXorlbyXZgiW5bplCExFxlnROaG21K12d8inpQ=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5 h1:GYE67ubwl+ZRw3CcXFUi49EwwQp6k+qS8sX0QuHDHO8=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5/go.mod h1:BMoGi2rG0udCCeaghqlNgiW3fTmT+TNnfTnBoWFYgcg=
github.com/jfrog/jfrog-cli-security v1.24.2 h1:nyI0lNYR8i6yZYeBDsBJnURYsMnFKEmt7QH4vaNxtGM=
github.com/jfrog/jfrog-cli-security v1.24.2/go.mod h1:3FXD5IkKtdQOm9CZk6cR7q0iC6PaGMnjqzZqRcQp2r0=
github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465 h1:Ff3BlNPndrAfa1xFI/ORFzfWTxQxF0buWG61PEJwd3U=
github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465/go.mod h1:WQ5Y+oKYyHFAlCbHN925bWhnShTd2ruxZ6YTpb76fpU=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260120063955-c654c159290e h1:F/VQ7UJ4jaEr9tLJ8jLfy4BF4Obhhd0pWu007SBSHt8=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260120063955-c654c159290e/go.mod h1:LbhCULfa/eIPSXNgQ5Xw8BIZRmJ0qfF2I4sPa7AHXkY=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260112010739-87fc7275623c h1:K9anqOZ7ASxlsijsl9u4jh92wqqIvJA4kTYfXrcOmJA=
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260112010739-87fc7275623c/go.mod h1:+Hnaikp/xCSPD/q7txxRy4Zc0wzjW/usrCSf+6uONSQ=
github.com/jfrog/jfrog-cli-security v1.26.0 h1:FcLshS1Ahm0++nV5q7UluFTCVRxH2wEIbqO7ZBag++I=
github.com/jfrog/jfrog-cli-security v1.26.0/go.mod h1:r9E0BdlNy6mq6gkRGslZRZaYe6WeGhLkpUm8+oEUOvU=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260120055025-12f25e12798a h1:tbHqd+9SJB6pMJn9aXkD4aMYfwsKwah5kuhZV6Q+e88=
github.com/jfrog/jfrog-client-go v1.55.1-0.20260120055025-12f25e12798a/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
Expand Down
23 changes: 17 additions & 6 deletions packagehandlers/commonpackageupdater.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package packagehandlers
import (
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
Expand All @@ -16,7 +17,6 @@ import (
"golang.org/x/exp/slices"
)

// PackageHandler interface to hold operations on packages
type PackageHandler interface {
UpdateDependency(details *utils.VulnerabilityDetails) error
}
Expand All @@ -32,7 +32,7 @@ func GetCompatiblePackageHandler(vulnDetails *utils.VulnerabilityDetails, detail
case techutils.Npm:
handler = &NpmPackageUpdater{}
case techutils.Yarn:
handler = &YarnPackageHandler{}
handler = &YarnPackageUpdater{}
case techutils.Pip:
handler = &PythonPackageHandler{pipRequirementsFile: defaultRequirementFile}
case techutils.Maven:
Expand All @@ -57,7 +57,6 @@ type CommonPackageHandler struct {
depsRepo string
}

// UpdateDependency updates the impacted package to the fixed version
func (cph *CommonPackageHandler) UpdateDependency(vulnDetails *utils.VulnerabilityDetails, installationCommand string, extraArgs ...string) (err error) {
// Lower the package name to avoid duplicates
impactedPackage := strings.ToLower(vulnDetails.ImpactedDependencyName)
Expand Down Expand Up @@ -89,9 +88,7 @@ func getFixedPackage(impactedPackage string, versionOperator string, suggestedFi
return
}

// Recursively scans the current directory for descriptor files based on the provided list of suffixes, while excluding paths that match the specified exclusion patterns.
// The patternsToExclude must be provided as regexp patterns. For instance, if the pattern ".*node_modules.*" is provided, any paths containing "node_modules" will be excluded from the result.
// Returns a slice of all discovered descriptor files, represented as absolute paths.
// patternsToExclude must be regexp patterns (e.g., ".*node_modules.*" excludes paths containing "node_modules")
func (cph *CommonPackageHandler) GetAllDescriptorFilesFullPaths(descriptorFilesSuffixes []string, patternsToExclude ...string) (descriptorFilesFullPaths []string, err error) {
if len(descriptorFilesSuffixes) == 0 {
return
Expand Down Expand Up @@ -149,3 +146,17 @@ func GetVulnerabilityLocations(vulnDetails *utils.VulnerabilityDetails, namesFil
}
return pathsSet.ToSlice()
}

func buildIsolatedEnv(envVars map[string]string) []string {
var env []string
for _, e := range os.Environ() {
key := strings.SplitN(e, "=", 2)[0]
if _, shouldOverride := envVars[key]; !shouldOverride {
env = append(env, e)
}
}
for key, value := range envVars {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
return env
}
164 changes: 164 additions & 0 deletions packagehandlers/nodepackageupdaterutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package packagehandlers

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/jfrog/frogbot/v2/utils"
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
"github.com/jfrog/jfrog-client-go/utils/log"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

const (
packageJsonFileName = "package.json"
dependenciesSectionName = "dependencies"
devDependenciesSectionName = "devDependencies"
optionalDependenciesSectionName = "optionalDependencies"
)

func GetDescriptorsToFixFromVulnerability(vulnDetails *utils.VulnerabilityDetails, lockFileName string) ([]string, error) {
// Get package.json paths from component locations
descriptorPaths := GetVulnerabilityLocations(vulnDetails, []string{packageJsonFileName})
if len(descriptorPaths) > 0 {
// Verify the corresponding lockfile exists in the same directory
var validDescriptorPaths []string
for _, descriptorPath := range descriptorPaths {
lockFilePath := filepath.Join(filepath.Dir(descriptorPath), lockFileName)
fileExists, err := fileutils.IsFileExists(lockFilePath, false)
if err != nil {
return nil, err
}
if fileExists {
validDescriptorPaths = append(validDescriptorPaths, descriptorPath)
}
}
if len(validDescriptorPaths) > 0 {
return validDescriptorPaths, nil
}
}

// Fallback: try to get lockfile paths and derive package.json from them (old behavior)
lockFilePaths := GetVulnerabilityLocations(vulnDetails, []string{lockFileName})
if len(lockFilePaths) == 0 {
return nil, fmt.Errorf("no location evidence was found for package %s", vulnDetails.ImpactedDependencyName)
}

return getPackageJsonPathsFromLockfilePaths(lockFilePaths)
}

func UpdatePackageAndRegenerateLock(packageName, oldVersion, newVersion, descriptorPath, originalWd, lockFileName string, allowedSections []string, regenerateLockfileFn func() error) error {
backupContent, err := updatePackageInDescriptor(packageName, newVersion, descriptorPath, allowedSections)
if err != nil {
return err
}

lockFileTracked, checkErr := utils.IsFileTrackedByGit(lockFileName, originalWd)
if checkErr != nil {
log.Debug(fmt.Sprintf("Failed to check if lock file is tracked in git: %s. Proceeding with lock file regeneration.", checkErr.Error()))
lockFileTracked = true
}

if !lockFileTracked {
log.Debug(fmt.Sprintf("Lock file '%s' does not exist in remote, skipping lock file regeneration", lockFileName))
log.Debug(fmt.Sprintf("Successfully updated '%s' from version '%s' to '%s' in descriptor '%s' without regenerating lock file", packageName, oldVersion, newVersion, descriptorPath))
return nil
}

if err = regenerateLockfile(packageName, newVersion, descriptorPath, originalWd, backupContent, regenerateLockfileFn); err != nil {
return err
}

log.Debug(fmt.Sprintf("Successfully updated '%s' from version '%s' to '%s' in descriptor '%s'", packageName, oldVersion, newVersion, descriptorPath))
return nil
}

// ==================== Internal Helper Functions ====================

func getPackageJsonPathsFromLockfilePaths(lockFilePaths []string) ([]string, error) {
var descriptorPaths []string
for _, lockFilePath := range lockFilePaths {
descriptorPath := filepath.Join(filepath.Dir(lockFilePath), packageJsonFileName)
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'", descriptorPath, lockFilePath)
}
descriptorPaths = append(descriptorPaths, descriptorPath)
}
return descriptorPaths, nil
}

func updatePackageInDescriptor(packageName, newVersion, descriptorPath string, allowedSections []string) ([]byte, error) {
descriptorContent, err := os.ReadFile(descriptorPath)
if err != nil {
return nil, fmt.Errorf("failed to read file '%s': %w", descriptorPath, err)
}

backupContent := make([]byte, len(descriptorContent))
copy(backupContent, descriptorContent)

updatedContent, err := updatePackageJsonDependency(descriptorContent, packageName, newVersion, allowedSections, descriptorPath)
if err != nil {
return nil, fmt.Errorf("failed to update version in descriptor: %w", err)
}

if err = os.WriteFile(descriptorPath, updatedContent, 0644); err != nil {
return nil, fmt.Errorf("failed to write updated descriptor '%s': %w", descriptorPath, err)
}
return backupContent, nil
}

func regenerateLockfile(packageName, newVersion, descriptorPath, originalWd string, backupContent []byte, regenerateLockfileFn func() error) (err error) {
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 = regenerateLockfileFn(); err != nil {
log.Warn(fmt.Sprintf("Failed to regenerate lock file after updating '%s' to version '%s': %s. Rolling back...", packageName, newVersion, 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
}
return nil
}

func updatePackageJsonDependency(content []byte, packageName, newVersion string, allowedSections []string, descriptorPath string) ([]byte, error) {
updated := false
escapedName := escapeJsonPathKey(packageName)

for _, section := range allowedSections {
path := section + "." + escapedName
if gjson.GetBytes(content, path).Exists() {
var err error
content, err = sjson.SetBytes(content, path, newVersion)
if err != nil {
return nil, fmt.Errorf("failed to set version for '%s' in section '%s': %w", packageName, section, err)
}
updated = true
}
}

if !updated {
return nil, fmt.Errorf("package '%s' not found in allowed sections [%s] in '%s'", packageName, strings.Join(allowedSections, ", "), descriptorPath)
}
return content, nil
}

func escapeJsonPathKey(key string) string {
r := strings.NewReplacer(".", "\\.", "*", "\\*", "?", "\\?")
return r.Replace(key)
}
Loading
Loading