github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/javascript/parse_yarn_lock.go (about) 1 package javascript 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "maps" 9 "regexp" 10 "slices" 11 "strings" 12 13 "github.com/goccy/go-yaml" 14 "github.com/scylladb/go-set/strset" 15 16 "github.com/anchore/syft/internal/log" 17 "github.com/anchore/syft/internal/unknown" 18 "github.com/anchore/syft/syft/artifact" 19 "github.com/anchore/syft/syft/file" 20 "github.com/anchore/syft/syft/pkg" 21 "github.com/anchore/syft/syft/pkg/cataloger/generic" 22 "github.com/anchore/syft/syft/pkg/cataloger/internal/dependency" 23 ) 24 25 var ( 26 // packageNameExp matches the name of the dependency in yarn.lock 27 // including scope/namespace prefix if found. 28 // For example: "aws-sdk@2.706.0" returns "aws-sdk" 29 // "@babel/code-frame@^7.0.0" returns "@babel/code-frame" 30 packageNameExp = regexp.MustCompile(`^"?((?:@\w[\w-_.]*\/)?\w[\w-_.]*)@`) 31 32 // packageURLExp matches the name and version of the dependency in yarn.lock 33 // from the resolved URL, including scope/namespace prefix if any. 34 // For example: 35 // `resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"` 36 // would return "async" and "3.2.3" 37 // 38 // `resolved "https://registry.yarnpkg.com/@4lolo/resize-observer-polyfill/-/resize-observer-polyfill-1.5.2.tgz#58868fc7224506236b5550d0c68357f0a874b84b"` 39 // would return "@4lolo/resize-observer-polyfill" and "1.5.2" 40 packageURLExp = regexp.MustCompile(`^resolved\s+"https://registry\.(?:yarnpkg\.com|npmjs\.org)/(.+?)/-/(?:.+?)-(\d+\..+?)\.tgz`) 41 42 // resolvedExp matches the resolved of the dependency in yarn.lock 43 // For example: 44 // resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" 45 // would return "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" 46 resolvedExp = regexp.MustCompile(`^resolved\s+"(.+?)"`) 47 ) 48 49 type yarnPackage struct { 50 Name string 51 Version string 52 Resolved string 53 Integrity string 54 Dependencies map[string]string // We don't currently support dependencies for yarn v1 lock files 55 } 56 57 type yarnV2PackageEntry struct { 58 Version string `yaml:"version"` 59 Resolution string `yaml:"resolution"` 60 Checksum string `yaml:"checksum"` 61 Dependencies map[string]string `yaml:"dependencies"` 62 } 63 64 type genericYarnLockAdapter struct { 65 cfg CatalogerConfig 66 } 67 68 func newGenericYarnLockAdapter(cfg CatalogerConfig) genericYarnLockAdapter { 69 return genericYarnLockAdapter{ 70 cfg: cfg, 71 } 72 } 73 74 func parseYarnV1LockFile(reader io.ReadCloser) ([]yarnPackage, error) { 75 content, err := io.ReadAll(reader) 76 if err != nil { 77 return nil, fmt.Errorf("failed to read yarn.lock file: %w", err) 78 } 79 80 re := regexp.MustCompile(`\r?\n`) 81 lines := re.Split(string(content), -1) 82 var pkgs []yarnPackage 83 var pkg = yarnPackage{} 84 var seenPkgs = strset.New() 85 dependencies := make(map[string]string) 86 87 for _, line := range lines { 88 if strings.HasPrefix(line, "#") { 89 continue 90 } 91 // Blank lines indicate the end of a package entry, so we add the package 92 // to the list and reset the dependencies 93 if len(line) == 0 && len(pkg.Name) > 0 && !seenPkgs.Has(pkg.Name+"@"+pkg.Version) { 94 pkg.Dependencies = dependencies 95 pkgs = append(pkgs, pkg) 96 seenPkgs.Add(pkg.Name + "@" + pkg.Version) 97 dependencies = make(map[string]string) 98 pkg = yarnPackage{} 99 continue 100 } 101 // The first line of a package entry is the name of the package with no 102 // leading spaces 103 if !strings.HasPrefix(line, " ") { 104 name := line 105 pkg.Name = findPackageName(name) 106 continue 107 } 108 if strings.HasPrefix(line, " ") && !strings.HasPrefix(line, " ") { 109 line = strings.Trim(line, " ") 110 array := strings.Split(line, " ") 111 switch array[0] { 112 case "version": 113 pkg.Version = strings.Trim(array[1], "\"") 114 case "resolved": 115 name, version, resolved := findResolvedPackageAndVersion(line) 116 if name != "" && version != "" && resolved != "" { 117 pkg.Name = name 118 pkg.Version = version 119 pkg.Resolved = resolved 120 } else { 121 pkg.Resolved = strings.Trim(array[1], "\"") 122 } 123 case "integrity": 124 pkg.Integrity = strings.Trim(array[1], "\"") 125 } 126 continue 127 } 128 if strings.HasPrefix(line, " ") { 129 line = strings.Trim(line, " ") 130 array := strings.Split(line, " ") 131 dependencyName := strings.Trim(array[0], "\"") 132 dependencyVersion := strings.Trim(array[1], "\"") 133 dependencies[dependencyName] = dependencyVersion 134 } 135 } 136 // If the last package in the list is not the same as the current package, add the current package 137 // to the list. In case there was no trailing new line before we hit EOF. 138 if len(pkg.Name) > 0 && !seenPkgs.Has(pkg.Name+"@"+pkg.Version) { 139 pkg.Dependencies = dependencies 140 pkgs = append(pkgs, pkg) 141 seenPkgs.Add(pkg.Name + "@" + pkg.Version) 142 } 143 144 return pkgs, nil 145 } 146 147 func parseYarnLockYaml(reader io.ReadCloser) ([]yarnPackage, error) { 148 var lockfile = map[string]yarnV2PackageEntry{} 149 if err := yaml.NewDecoder(reader, yaml.AllowDuplicateMapKey()).Decode(&lockfile); err != nil { 150 return nil, fmt.Errorf("failed to unmarshal yarn v2 lockfile: %w", err) 151 } 152 153 packages := make(map[string]yarnPackage) 154 for key, value := range lockfile { 155 packageName := findPackageName(key) 156 if packageName == "" { 157 log.WithFields("key", key).Error("unable to parse yarn v2 package key") 158 continue 159 } 160 161 packages[packageName] = yarnPackage{Name: packageName, Version: value.Version, Resolved: value.Resolution, Integrity: value.Checksum, Dependencies: value.Dependencies} 162 } 163 164 return slices.Collect(maps.Values(packages)), nil 165 } 166 167 func (a genericYarnLockAdapter) parseYarnLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 168 // in the case we find yarn.lock files in the node_modules directories, skip those 169 // as the whole purpose of the lock file is for the specific dependencies of the project 170 if pathContainsNodeModulesDirectory(reader.Path()) { 171 return nil, nil, nil 172 } 173 174 data, err := io.ReadAll(reader) 175 if err != nil { 176 return nil, nil, fmt.Errorf("failed to load yarn.lock file: %w", err) 177 } 178 // Reset the reader to the beginning of the file 179 reader.ReadCloser = io.NopCloser(bytes.NewBuffer(data)) 180 181 var yarnPkgs []yarnPackage 182 // v1 Yarn lockfiles are not YAML, so we need to parse them as a special case. They typically 183 // include a comment line that indicates the version. I.e. "# yarn lockfile v1" 184 if strings.Contains(string(data), "# yarn lockfile v1") { 185 yarnPkgs, err = parseYarnV1LockFile(reader) 186 } else { 187 yarnPkgs, err = parseYarnLockYaml(reader) 188 } 189 if err != nil { 190 return nil, nil, fmt.Errorf("failed to parse yarn.lock file: %w", err) 191 } 192 193 packages := make([]pkg.Package, len(yarnPkgs)) 194 for i, p := range yarnPkgs { 195 packages[i] = newYarnLockPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version, p.Resolved, p.Integrity, p.Dependencies) 196 } 197 198 pkg.Sort(packages) 199 200 return packages, dependency.Resolve(yarnLockDependencySpecifier, packages), unknown.IfEmptyf(packages, "unable to determine packages") 201 } 202 203 func findPackageName(line string) string { 204 if matches := packageNameExp.FindStringSubmatch(line); len(matches) >= 2 { 205 return matches[1] 206 } 207 208 return "" 209 } 210 211 func findResolvedPackageAndVersion(line string) (string, string, string) { 212 var resolved string 213 if matches := resolvedExp.FindStringSubmatch(line); len(matches) >= 2 { 214 resolved = matches[1] 215 } 216 if matches := packageURLExp.FindStringSubmatch(line); len(matches) >= 2 { 217 return matches[1], matches[2], resolved 218 } 219 220 return "", "", "" 221 }