11package packageupdaters
22
33import (
4+ "errors"
45 "fmt"
56 "io/fs"
7+ "os"
68 "os/exec"
79 "path/filepath"
810 "regexp"
911 "strings"
12+ "time"
1013
1114 "github.com/jfrog/gofrog/datastructures"
1215 "github.com/jfrog/jfrog-cli-security/utils/techutils"
1316 "github.com/jfrog/jfrog-client-go/utils/log"
17+ "github.com/tidwall/gjson"
18+ "github.com/tidwall/sjson"
1419 "golang.org/x/exp/slices"
1520
1621 "github.com/jfrog/frogbot/v2/utils"
1722)
1823
24+ // Node
25+ const (
26+ nodePackageJSONFileName = "package.json"
27+ nodeModulesDirName = "node_modules"
28+ nodeDependenciesSection = "dependencies"
29+ nodeDevDependenciesSection = "devDependencies"
30+ nodeOptionalDependenciesSection = "optionalDependencies"
31+ nodeOverridesSection = "overrides"
32+ nodePackageManagerInstallTimeout = 15 * time .Minute
33+ )
34+
35+ var nodePackageManifestSections = []string {
36+ nodeDependenciesSection ,
37+ nodeDevDependenciesSection ,
38+ nodeOptionalDependenciesSection ,
39+ nodeOverridesSection ,
40+ }
41+
1942// PackageUpdater interface to hold operations on packages
2043type PackageUpdater interface {
2144 UpdateDependency (details * utils.VulnerabilityDetails ) error
@@ -54,6 +77,124 @@ func GetCompatiblePackageUpdater(vulnDetails *utils.VulnerabilityDetails, detail
5477// TODO can be deleted if not needed after refactoring all package updaters
5578type CommonPackageUpdater struct {}
5679
80+ // evidencePathLooksLikeNpmPackageCoordinate detects scanner evidence paths like "lodash@4.17.19/package.json" (not real paths). Pnpm filters these; npm does not.
81+ func evidencePathLooksLikeNpmPackageCoordinate (evidenceFile string ) bool {
82+ dir := filepath .Dir (evidenceFile )
83+ if dir == "." || dir == "" {
84+ return false
85+ }
86+ for _ , part := range strings .Split (filepath .ToSlash (dir ), "/" ) {
87+ if part == "" || part == "." {
88+ continue
89+ }
90+ if strings .Contains (part , "@" ) && ! strings .HasPrefix (part , "@" ) {
91+ return true
92+ }
93+ }
94+ return false
95+ }
96+
97+ func (cph * CommonPackageUpdater ) CollectVulnerabilityDescriptorPaths (vulnDetails * utils.VulnerabilityDetails , namesFilters []string , ignoreFilters []string ) []string {
98+ pathsSet := datastructures .MakeSet [string ]()
99+ for _ , component := range vulnDetails .Components {
100+ for _ , evidence := range component .Evidences {
101+ if evidence .File == "" || techutils .IsTechnologyDescriptor (evidence .File ) == techutils .NoTech || slices .ContainsFunc (ignoreFilters , func (pattern string ) bool { return strings .Contains (evidence .File , pattern ) }) {
102+ continue
103+ }
104+ if len (namesFilters ) == 0 || slices .Contains (namesFilters , filepath .Base (evidence .File )) {
105+ pathsSet .Add (evidence .File )
106+ }
107+ }
108+ }
109+ return pathsSet .ToSlice ()
110+ }
111+
112+ // BuildPackageDependencyLineRegex builds a regexp for matching a dependency line in a manifest.
113+ func (cph * CommonPackageUpdater ) BuildPackageDependencyLineRegex (impactedName , impactedVersion , dependencyLineFormat string ) * regexp.Regexp {
114+ regexpFitImpactedName := strings .ToLower (regexp .QuoteMeta (impactedName ))
115+ regexpFitImpactedVersion := strings .ToLower (regexp .QuoteMeta (impactedVersion ))
116+ regexpCompleteFormat := fmt .Sprintf (strings .ToLower (dependencyLineFormat ), regexpFitImpactedName , regexpFitImpactedVersion )
117+ return regexp .MustCompile (regexpCompleteFormat )
118+ }
119+
120+ func escapeJsonPathKey (key string ) string {
121+ r := strings .NewReplacer ("." , "\\ ." , "*" , "\\ *" , "?" , "\\ ?" )
122+ return r .Replace (key )
123+ }
124+
125+ // GetFixedPackageJSONManifest returns manifest bytes with packageName set to newVersion in allowed sections.
126+ func (cph * CommonPackageUpdater ) GetFixedPackageJSONManifest (content []byte , packageName , newVersion , descriptorPath string ) ([]byte , error ) {
127+ updated := false
128+ escapedName := escapeJsonPathKey (packageName )
129+
130+ for _ , section := range nodePackageManifestSections {
131+ path := section + "." + escapedName
132+ if gjson .GetBytes (content , path ).Exists () {
133+ var err error
134+ content , err = sjson .SetBytes (content , path , newVersion )
135+ if err != nil {
136+ return nil , fmt .Errorf ("failed to set version for '%s' in section '%s': %w" , packageName , section , err )
137+ }
138+ updated = true
139+ }
140+ }
141+
142+ if ! updated {
143+ return nil , fmt .Errorf ("package '%s' not found in allowed sections [%s] in '%s'" , packageName , strings .Join (nodePackageManifestSections , ", " ), descriptorPath )
144+ }
145+ return content , nil
146+ }
147+
148+ // UpdatePackageJSONDescriptor writes the fixed version for packageName to descriptorPath and returns original file bytes for rollback.
149+ func (cph * CommonPackageUpdater ) UpdatePackageJSONDescriptor (descriptorPath , packageName , newVersion string ) ([]byte , error ) {
150+ //#nosec G304 -- descriptorPath comes from vulnerability evidence in the scanned repository.
151+ descriptorContent , err := os .ReadFile (descriptorPath )
152+ if err != nil {
153+ return nil , fmt .Errorf ("failed to read file '%s': %w" , descriptorPath , err )
154+ }
155+
156+ backupContent := make ([]byte , len (descriptorContent ))
157+ copy (backupContent , descriptorContent )
158+
159+ updatedContent , err := cph .GetFixedPackageJSONManifest (descriptorContent , packageName , newVersion , descriptorPath )
160+ if err != nil {
161+ return nil , fmt .Errorf ("failed to update version in descriptor: %w" , err )
162+ }
163+
164+ //#nosec G306 G703 -- 0644 for checked-out source; path same trusted source as ReadFile above.
165+ if err = os .WriteFile (descriptorPath , updatedContent , 0644 ); err != nil {
166+ return nil , fmt .Errorf ("failed to write updated descriptor '%s': %w" , descriptorPath , err )
167+ }
168+ return backupContent , nil
169+ }
170+
171+ func (cph * CommonPackageUpdater ) withDescriptorWorkingDir (descriptorPath , originalWd string , fn func () error ) (err error ) {
172+ descriptorDir := filepath .Dir (descriptorPath )
173+ if err = os .Chdir (descriptorDir ); err != nil {
174+ return fmt .Errorf ("failed to change directory to '%s': %w" , descriptorDir , err )
175+ }
176+ defer func () {
177+ if chErr := os .Chdir (originalWd ); chErr != nil {
178+ err = errors .Join (err , fmt .Errorf ("failed to return to original directory: %w" , chErr ))
179+ }
180+ }()
181+ return fn ()
182+ }
183+
184+ func (cph * CommonPackageUpdater ) buildEnvWithOverrides (overrides map [string ]string ) []string {
185+ env := make ([]string , 0 , len (os .Environ ())+ len (overrides ))
186+ for _ , e := range os .Environ () {
187+ key := strings .SplitN (e , "=" , 2 )[0 ]
188+ if _ , shouldOverride := overrides [key ]; ! shouldOverride {
189+ env = append (env , e )
190+ }
191+ }
192+ for key , value := range overrides {
193+ env = append (env , fmt .Sprintf ("%s=%s" , key , value ))
194+ }
195+ return env
196+ }
197+
57198// UpdateDependency updates the impacted package to the fixed version
58199func (cph * CommonPackageUpdater ) UpdateDependency (vulnDetails * utils.VulnerabilityDetails , installationCommand string , extraArgs ... string ) (err error ) {
59200 // Lower the package name to avoid duplicates
@@ -70,13 +211,31 @@ func runPackageMangerCommand(commandName string, techName string, commandArgs []
70211 fullCommand := commandName + " " + strings .Join (commandArgs , " " )
71212 log .Debug (fmt .Sprintf ("Running '%s'" , fullCommand ))
72213 //#nosec G204 -- False positive - the subprocess only runs after the user's approval.
73- output , err := exec .Command (commandName , commandArgs ... ).CombinedOutput ()
214+ cmd := exec .Command (commandName , commandArgs ... )
215+ if commandName == "pnpm" {
216+ cmd .Env = envWithCorepackIntegrityWorkaround (os .Environ ())
217+ }
218+ output , err := cmd .CombinedOutput ()
74219 if err != nil {
75220 return fmt .Errorf ("failed to update %s dependency: '%s' command failed: %s\n %s" , techName , fullCommand , err .Error (), output )
76221 }
77222 return nil
78223}
79224
225+ // envWithCorepackIntegrityWorkaround sets COREPACK_INTEGRITY_KEYS=0 for older Node/Corepack (e.g. corepack#612).
226+ // Also applied after buildEnvWithOverrides for pnpm lockfile regeneration so Corepack-invoked pnpm matches runPackageMangerCommand behavior.
227+ func envWithCorepackIntegrityWorkaround (base []string ) []string {
228+ const key = "COREPACK_INTEGRITY_KEYS"
229+ prefix := key + "="
230+ out := make ([]string , 0 , len (base )+ 1 )
231+ for _ , e := range base {
232+ if ! strings .HasPrefix (e , prefix ) {
233+ out = append (out , e )
234+ }
235+ }
236+ return append (out , prefix + "0" )
237+ }
238+
80239// Returns the updated package and version as it should be run in the update command:
81240// If the package manager expects a single string (example: <packName>@<version>) it returns []string{<packName>@<version>}
82241// If the command args suppose to be seperated by spaces (example: <packName> -v <version>) it returns []string{<packName>, "-v", <version>}
@@ -129,23 +288,11 @@ func (cph *CommonPackageUpdater) GetAllDescriptorFilesFullPaths(descriptorFilesS
129288}
130289
131290func BuildPackageWithVersionRegex (impactedName , impactedVersion , dependencyLineFormat string ) * regexp.Regexp {
132- regexpFitImpactedName := strings .ToLower (regexp .QuoteMeta (impactedName ))
133- regexpFitImpactedVersion := strings .ToLower (regexp .QuoteMeta (impactedVersion ))
134- regexpCompleteFormat := fmt .Sprintf (strings .ToLower (dependencyLineFormat ), regexpFitImpactedName , regexpFitImpactedVersion )
135- return regexp .MustCompile (regexpCompleteFormat )
291+ var c CommonPackageUpdater
292+ return c .BuildPackageDependencyLineRegex (impactedName , impactedVersion , dependencyLineFormat )
136293}
137294
138295func GetVulnerabilityLocations (vulnDetails * utils.VulnerabilityDetails , namesFilters []string , ignoreFilters []string ) []string {
139- pathsSet := datastructures .MakeSet [string ]()
140- for _ , component := range vulnDetails .Components {
141- for _ , evidence := range component .Evidences {
142- if evidence .File == "" || techutils .IsTechnologyDescriptor (evidence .File ) == techutils .NoTech || slices .ContainsFunc (ignoreFilters , func (pattern string ) bool { return strings .Contains (evidence .File , pattern ) }) {
143- continue
144- }
145- if len (namesFilters ) == 0 || slices .Contains (namesFilters , filepath .Base (evidence .File )) {
146- pathsSet .Add (evidence .File )
147- }
148- }
149- }
150- return pathsSet .ToSlice ()
296+ var c CommonPackageUpdater
297+ return c .CollectVulnerabilityDescriptorPaths (vulnDetails , namesFilters , ignoreFilters )
151298}
0 commit comments