github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/cataloger/javascript/parse_pnpm_lock.go (about) 1 package javascript 2 3 import ( 4 "io" 5 "regexp" 6 "strconv" 7 "strings" 8 9 "gopkg.in/yaml.v3" 10 11 "github.com/anchore/syft/internal/log" 12 "github.com/anchore/syft/syft/artifact" 13 "github.com/anchore/syft/syft/file" 14 "github.com/anchore/syft/syft/pkg" 15 "github.com/anchore/syft/syft/pkg/cataloger/generic" 16 "github.com/anchore/syft/syft/pkg/cataloger/javascript/key" 17 ) 18 19 // integrity check 20 var _ generic.Parser = parsePnpmLock 21 22 type pnpmLockPackage struct { 23 Name string 24 Version string 25 Integrity string 26 Resolved string 27 Dependencies map[string]string 28 } 29 30 type pnpmLockYaml struct { 31 Version string `yaml:"lockfileVersion"` 32 Specifiers map[string]interface{} `yaml:"specifiers"` 33 Dependencies map[string]interface{} `yaml:"dependencies"` 34 DevDependencies map[string]interface{} `yaml:"devDependencies"` 35 Packages map[string]*pnpmLockPackage `yaml:"packages"` 36 } 37 38 func newPnpmLockPackage(resolver file.Resolver, location file.Location, p *pnpmLockPackage) pkg.Package { 39 if p == nil { 40 return pkg.Package{} 41 } 42 43 return finalizeLockPkg( 44 resolver, 45 location, 46 pkg.Package{ 47 Name: p.Name, 48 Version: p.Version, 49 Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), 50 PURL: packageURL(p.Name, p.Version), 51 MetadataType: pkg.NpmPackageLockJSONMetadataType, 52 Language: pkg.JavaScript, 53 Type: pkg.NpmPkg, 54 Metadata: pkg.NpmPackageLockJSONMetadata{ 55 Resolved: p.Resolved, 56 Integrity: p.Integrity, 57 }, 58 }, 59 ) 60 } 61 62 func parsePnpmLock(resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 63 pnpmMap, pnpmLock := parsePnpmLockFile(reader) 64 pkgs, _ := finalizePnpmLockWithoutPackageJSON(resolver, &pnpmLock, pnpmMap, reader.Location) 65 return pkgs, nil, nil 66 } 67 68 func finalizePnpmLockWithoutPackageJSON(resolver file.Resolver, _ *pnpmLockYaml, pnpmMap map[string]*pnpmLockPackage, indexLocation file.Location) ([]pkg.Package, []artifact.Relationship) { 69 seenPkgMap := make(map[string]bool) 70 var pkgs []pkg.Package 71 var relationships []artifact.Relationship 72 73 name := rootNameFromPath(indexLocation) 74 if name == "" { 75 return nil, nil 76 } 77 root := newPnpmLockPackage(resolver, indexLocation, &pnpmLockPackage{Name: name, Version: "0.0.0"}) 78 pkgs = append(pkgs, root) 79 80 for _, lockPkg := range pnpmMap { 81 if seenPkgMap[key.NpmPackageKey(lockPkg.Name, lockPkg.Version)] { 82 continue 83 } 84 85 pkg := newPnpmLockPackage(resolver, indexLocation, lockPkg) 86 pkgs = append(pkgs, pkg) 87 seenPkgMap[key.NpmPackageKey(pkg.Name, pkg.Version)] = true 88 } 89 90 pkg.Sort(pkgs) 91 pkg.SortRelationships(relationships) 92 return pkgs, relationships 93 } 94 95 func finalizePnpmLockWithPackageJSON(resolver file.Resolver, pkgjson *packageJSON, pnpmLock *pnpmLockYaml, pnpmMap map[string]*pnpmLockPackage, indexLocation file.Location) ([]pkg.Package, []artifact.Relationship) { 96 seenPkgMap := make(map[string]bool) 97 var pkgs []pkg.Package 98 var relationships []artifact.Relationship 99 100 root := newPnpmLockPackage(resolver, indexLocation, &pnpmLockPackage{Name: pkgjson.Name, Version: pkgjson.Version}) 101 pkgs = append(pkgs, root) 102 103 // create root relationships 104 for name, info := range pnpmLock.Dependencies { 105 version := parsePnpmDependencyInfo(info) 106 if version == "" { 107 continue 108 } 109 110 p := pnpmMap[key.NpmPackageKey(name, version)] 111 pkg := newPnpmLockPackage(resolver, indexLocation, p) 112 rel := artifact.Relationship{ 113 From: pkg, 114 To: root, 115 Type: artifact.DependencyOfRelationship, 116 } 117 relationships = append(relationships, rel) 118 } 119 120 // create packages 121 for _, lockPkg := range pnpmMap { 122 if seenPkgMap[key.NpmPackageKey(lockPkg.Name, lockPkg.Version)] { 123 continue 124 } 125 126 pkg := newPnpmLockPackage(resolver, indexLocation, lockPkg) 127 pkgs = append(pkgs, pkg) 128 seenPkgMap[key.NpmPackageKey(pkg.Name, pkg.Version)] = true 129 } 130 131 // create pkg relationships 132 for _, lockPkg := range pnpmMap { 133 p := pnpmMap[key.NpmPackageKey(lockPkg.Name, lockPkg.Version)] 134 pkg := newPnpmLockPackage( 135 resolver, 136 indexLocation, 137 p, 138 ) 139 140 for name, version := range lockPkg.Dependencies { 141 dep := pnpmMap[key.NpmPackageKey(name, version)] 142 depPkg := newPnpmLockPackage( 143 resolver, 144 indexLocation, 145 dep, 146 ) 147 rel := artifact.Relationship{ 148 From: depPkg, 149 To: pkg, 150 Type: artifact.DependencyOfRelationship, 151 } 152 relationships = append(relationships, rel) 153 } 154 } 155 156 pkg.Sort(pkgs) 157 pkg.SortRelationships(relationships) 158 return pkgs, relationships 159 } 160 161 func parsePnpmPackages(lockFile pnpmLockYaml, lockVersion float64, pnpmLock map[string]*pnpmLockPackage) { 162 packageNameRegex := regexp.MustCompile(`^/?([^(]*)(?:\(.*\))*$`) 163 splitChar := "/" 164 if lockVersion >= 6.0 { 165 splitChar = "@" 166 } 167 168 for nameVersion, packageDetails := range lockFile.Packages { 169 nameVersion = packageNameRegex.ReplaceAllString(nameVersion, "$1") 170 nameVersionSplit := strings.Split(strings.TrimPrefix(nameVersion, "/"), splitChar) 171 172 // last element in split array is version 173 version := nameVersionSplit[len(nameVersionSplit)-1] 174 175 // construct name from all array items other than last item (version) 176 name := strings.Join(nameVersionSplit[:len(nameVersionSplit)-1], splitChar) 177 178 if pnpmLock[key.NpmPackageKey(name, version)] != nil { 179 if pnpmLock[key.NpmPackageKey(name, version)].Version == version { 180 continue 181 } 182 } 183 184 packageDetails.Name = name 185 packageDetails.Version = version 186 187 pnpmLock[key.NpmPackageKey(name, version)] = packageDetails 188 } 189 } 190 191 func parsePnpmDependencyInfo(info interface{}) (version string) { 192 switch info := info.(type) { 193 case string: 194 version = info 195 case map[string]interface{}: 196 v, ok := info["version"] 197 if !ok { 198 break 199 } 200 ver, ok := v.(string) 201 if ok { 202 version = parseVersion(ver) 203 } 204 } 205 log.Tracef("unsupported pnpm dependency type: %+v", info) 206 return 207 } 208 209 func parsePnpmDependencies(lockFile pnpmLockYaml, pnpmLock map[string]*pnpmLockPackage) { 210 for name, info := range lockFile.Dependencies { 211 version := parsePnpmDependencyInfo(info) 212 if version == "" { 213 continue 214 } 215 216 if pnpmLock[key.NpmPackageKey(name, version)] != nil { 217 if pnpmLock[key.NpmPackageKey(name, version)].Version == version { 218 continue 219 } 220 } 221 222 pnpmLock[key.NpmPackageKey(name, version)] = &pnpmLockPackage{ 223 Name: name, 224 Version: version, 225 } 226 } 227 } 228 229 // parsePnpmLock parses a pnpm-lock.yaml file to get a list of packages 230 func parsePnpmLockFile(file file.LocationReadCloser) (map[string]*pnpmLockPackage, pnpmLockYaml) { 231 pnpmLock := map[string]*pnpmLockPackage{} 232 bytes, err := io.ReadAll(file) 233 if err != nil { 234 return pnpmLock, pnpmLockYaml{} 235 } 236 237 var lockFile pnpmLockYaml 238 if err := yaml.Unmarshal(bytes, &lockFile); err != nil { 239 return pnpmLock, pnpmLockYaml{} 240 } 241 242 lockVersion, _ := strconv.ParseFloat(lockFile.Version, 64) 243 244 // parse packages from packages section of pnpm-lock.yaml 245 parsePnpmPackages(lockFile, lockVersion, pnpmLock) 246 parsePnpmDependencies(lockFile, pnpmLock) 247 248 return pnpmLock, lockFile 249 } 250 251 func parseVersion(version string) string { 252 return strings.SplitN(version, "(", 2)[0] 253 }