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 }