github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/lockfile/npm/packagelockjsonv1.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package npm
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"slices"
    23  	"strings"
    24  
    25  	"deps.dev/util/resolve"
    26  	"deps.dev/util/resolve/dep"
    27  	"github.com/google/osv-scalibr/clients/datasource"
    28  	"github.com/google/osv-scalibr/guidedremediation/internal/manifest/npm"
    29  	"github.com/google/osv-scalibr/internal/dependencyfile/packagelockjson"
    30  	"github.com/tidwall/gjson"
    31  	"github.com/tidwall/sjson"
    32  )
    33  
    34  // nodesFromDependencies extracts graph from old-style (npm < 7 / lockfileVersion 1) dependencies structure
    35  // https://docs.npmjs.com/cli/v6/configuring-npm/package-lock-json
    36  // Installed packages stored in recursive "dependencies" object
    37  // with "requires" field listing direct dependencies, and each possibly having their own "dependencies"
    38  // No dependency information package-lock.json for the root node, so we must also have the package.json
    39  func nodesFromDependencies(lockJSON packagelockjson.LockFile, packageJSON io.Reader) (*resolve.Graph, *nodeModule, error) {
    40  	// Need to grab the root requirements from the package.json, since it's not in the lockfile
    41  	var manifestJSON npm.PackageJSON
    42  	if err := json.NewDecoder(packageJSON).Decode(&manifestJSON); err != nil {
    43  		return nil, nil, err
    44  	}
    45  
    46  	nodeModuleTree := &nodeModule{
    47  		Children: make(map[string]*nodeModule),
    48  		Deps:     make(map[string]dependencyVersionSpec),
    49  	}
    50  
    51  	// The order we process dependency types here is to match npm's behavior.
    52  	for name, version := range manifestJSON.PeerDependencies {
    53  		var typ dep.Type
    54  		typ.AddAttr(dep.Scope, "peer")
    55  		if manifestJSON.PeerDependenciesMeta[name].Optional {
    56  			typ.AddAttr(dep.Opt, "")
    57  		}
    58  		nodeModuleTree.Deps[name] = dependencyVersionSpec{Version: version, DepType: typ}
    59  	}
    60  	for name, version := range manifestJSON.Dependencies {
    61  		nodeModuleTree.Deps[name] = dependencyVersionSpec{Version: version}
    62  	}
    63  	for name, version := range manifestJSON.OptionalDependencies {
    64  		nodeModuleTree.Deps[name] = dependencyVersionSpec{Version: version, DepType: dep.NewType(dep.Opt)}
    65  	}
    66  	for name, version := range manifestJSON.DevDependencies {
    67  		nodeModuleTree.Deps[name] = dependencyVersionSpec{Version: version, DepType: dep.NewType(dep.Dev)}
    68  	}
    69  	reVersionAliasedDeps(nodeModuleTree.Deps)
    70  
    71  	g := &resolve.Graph{}
    72  	nodeModuleTree.NodeID = g.AddNode(resolve.VersionKey{
    73  		PackageKey: resolve.PackageKey{
    74  			System: resolve.NPM,
    75  			Name:   manifestJSON.Name,
    76  		},
    77  		VersionType: resolve.Concrete,
    78  		Version:     manifestJSON.Version,
    79  	})
    80  
    81  	err := computeDependenciesRecursive(g, nodeModuleTree, lockJSON.Dependencies)
    82  
    83  	return g, nodeModuleTree, err
    84  }
    85  
    86  func computeDependenciesRecursive(g *resolve.Graph, parent *nodeModule, deps map[string]packagelockjson.Dependency) error {
    87  	for name, d := range deps {
    88  		actualName, version := npm.SplitNPMAlias(d.Version)
    89  		nID := g.AddNode(resolve.VersionKey{
    90  			PackageKey: resolve.PackageKey{
    91  				System: resolve.NPM,
    92  				Name:   name,
    93  			},
    94  			VersionType: resolve.Concrete,
    95  			Version:     version,
    96  		})
    97  		nm := &nodeModule{
    98  			Parent:     parent,
    99  			NodeID:     nID,
   100  			Children:   make(map[string]*nodeModule),
   101  			Deps:       make(map[string]dependencyVersionSpec),
   102  			ActualName: actualName,
   103  		}
   104  
   105  		// The requires map includes regular dependencies AND optionalDependencies
   106  		// but it does not include peerDependencies or devDependencies.
   107  		// The generated graphs will lack the edges between peers
   108  		for name, version := range d.Requires {
   109  			nm.Deps[name] = dependencyVersionSpec{Version: version}
   110  		}
   111  		reVersionAliasedDeps(nm.Deps)
   112  
   113  		parent.Children[name] = nm
   114  		if d.Dependencies != nil {
   115  			if err := computeDependenciesRecursive(g, nm, d.Dependencies); err != nil {
   116  				return err
   117  			}
   118  		}
   119  	}
   120  
   121  	return nil
   122  }
   123  
   124  // writeDependencies writes the patches to the "dependencies" section (v1) of the lockfile (if it exists).
   125  func writeDependencies(lockf []byte, patchMap map[string]map[string]string, api *datasource.NPMRegistryAPIClient) ([]byte, error) {
   126  	if !gjson.GetBytes(lockf, "packages").Exists() {
   127  		return lockf, nil
   128  	}
   129  	// Check if the lockfile is using CRLF or LF by checking the first newline.
   130  	i := slices.Index(lockf, byte('\n'))
   131  	crlf := i > 0 && lockf[i-1] == '\r'
   132  
   133  	return writeDependenciesRecursive(lockf, patchMap, api, "dependencies", 1, crlf)
   134  }
   135  
   136  func writeDependenciesRecursive(lockf []byte, patchMap map[string]map[string]string, api *datasource.NPMRegistryAPIClient, path string, depth int, crlf bool) ([]byte, error) {
   137  	for pkg, data := range gjson.GetBytes(lockf, path).Map() {
   138  		pkgPath := path + "." + gjson.Escape(pkg)
   139  		if data.Get("dependencies").Exists() {
   140  			var err error
   141  			lockf, err = writeDependenciesRecursive(lockf, patchMap, api, pkgPath+".dependencies", depth+1, crlf)
   142  			if err != nil {
   143  				return nil, err
   144  			}
   145  		}
   146  		isAlias := false
   147  		realPkg, version := npm.SplitNPMAlias(data.Get("version").String())
   148  		if realPkg != "" {
   149  			isAlias = true
   150  			pkg = realPkg
   151  		}
   152  
   153  		if upgrades, ok := patchMap[pkg]; ok {
   154  			if version, ok := upgrades[version]; ok {
   155  				// update dependency in place
   156  				npmData, err := api.FullJSON(context.Background(), pkg, version)
   157  				if err != nil {
   158  					return lockf, err
   159  				}
   160  				// The only necessary fields to update appear to be "version", "resolved", "integrity", and "requires"
   161  				newVersion := npmData.Get("version").String()
   162  				if isAlias {
   163  					newVersion = "npm:" + pkg + "@" + newVersion
   164  				}
   165  				// These shouldn't error.
   166  				lockf, _ = sjson.SetBytes(lockf, pkgPath+".version", newVersion)
   167  				lockf, _ = sjson.SetBytes(lockf, pkgPath+".resolved", npmData.Get("dist.tarball").String())
   168  				lockf, _ = sjson.SetBytes(lockf, pkgPath+".integrity", npmData.Get("dist.integrity").String())
   169  				// formatting & padding to output for the correct level at this depth
   170  				pretty := fmt.Sprintf("|@pretty:{\"prefix\": %q}", strings.Repeat(" ", 4*depth+2))
   171  				reqs := npmData.Get("dependencies" + pretty)
   172  				if !reqs.Exists() {
   173  					lockf, _ = sjson.DeleteBytes(lockf, pkgPath+".requires")
   174  				} else {
   175  					text := reqs.Raw
   176  					// remove trailing newlines that @pretty creates for objects
   177  					text = strings.TrimSuffix(text, "\n")
   178  					if crlf {
   179  						text = strings.ReplaceAll(text, "\n", "\r\n")
   180  					}
   181  					lockf, _ = sjson.SetRawBytes(lockf, pkgPath+".requires", []byte(text))
   182  				}
   183  			}
   184  		}
   185  	}
   186  
   187  	return lockf, nil
   188  }