github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/pkg/cataloger/javascript/parse_yarn_lock.go (about) 1 package javascript 2 3 import ( 4 "bufio" 5 "fmt" 6 "regexp" 7 8 "github.com/scylladb/go-set/strset" 9 10 "github.com/anchore/syft/syft/artifact" 11 "github.com/anchore/syft/syft/file" 12 "github.com/anchore/syft/syft/pkg" 13 "github.com/anchore/syft/syft/pkg/cataloger/generic" 14 ) 15 16 // integrity check 17 var _ generic.Parser = parseYarnLock 18 19 var ( 20 // packageNameExp matches the name of the dependency in yarn.lock 21 // including scope/namespace prefix if found. 22 // For example: "aws-sdk@2.706.0" returns "aws-sdk" 23 // "@babel/code-frame@^7.0.0" returns "@babel/code-frame" 24 packageNameExp = regexp.MustCompile(`^"?((?:@\w[\w-_.]*\/)?\w[\w-_.]*)@`) 25 26 // versionExp matches the "version" line of a yarn.lock entry and captures the version value. 27 // For example: version "4.10.1" (...and the value "4.10.1" is captured) 28 versionExp = regexp.MustCompile(`^\W+version(?:\W+"|:\W+)([\w-_.]+)"?`) 29 30 // packageURLExp matches the name and version of the dependency in yarn.lock 31 // from the resolved URL, including scope/namespace prefix if any. 32 // For example: 33 // `resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"` 34 // would return "async" and "3.2.3" 35 // 36 // `resolved "https://registry.yarnpkg.com/@4lolo/resize-observer-polyfill/-/resize-observer-polyfill-1.5.2.tgz#58868fc7224506236b5550d0c68357f0a874b84b"` 37 // would return "@4lolo/resize-observer-polyfill" and "1.5.2" 38 packageURLExp = regexp.MustCompile(`^\s+resolved\s+"https://registry\.(?:yarnpkg\.com|npmjs\.org)/(.+?)/-/(?:.+?)-(\d+\..+?)\.tgz`) 39 ) 40 41 const ( 42 noPackage = "" 43 noVersion = "" 44 ) 45 46 func parseYarnLock(resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { 47 // in the case we find yarn.lock files in the node_modules directories, skip those 48 // as the whole purpose of the lock file is for the specific dependencies of the project 49 if pathContainsNodeModulesDirectory(reader.Path()) { 50 return nil, nil, nil 51 } 52 53 var pkgs []pkg.Package 54 scanner := bufio.NewScanner(reader) 55 parsedPackages := strset.New() 56 currentPackage := noPackage 57 currentVersion := noVersion 58 59 for scanner.Scan() { 60 line := scanner.Text() 61 62 if packageName := findPackageName(line); packageName != noPackage { 63 // When we find a new package, check if we have unsaved identifiers 64 if currentPackage != noPackage && currentVersion != noVersion && !parsedPackages.Has(currentPackage+"@"+currentVersion) { 65 pkgs = append(pkgs, newYarnLockPackage(resolver, reader.Location, currentPackage, currentVersion)) 66 parsedPackages.Add(currentPackage + "@" + currentVersion) 67 } 68 69 currentPackage = packageName 70 } else if version := findPackageVersion(line); version != noVersion { 71 currentVersion = version 72 } else if packageName, version := findPackageAndVersion(line); packageName != noPackage && version != noVersion && !parsedPackages.Has(packageName+"@"+version) { 73 pkgs = append(pkgs, newYarnLockPackage(resolver, reader.Location, packageName, version)) 74 parsedPackages.Add(packageName + "@" + version) 75 76 // Cleanup to indicate no unsaved identifiers 77 currentPackage = noPackage 78 currentVersion = noVersion 79 } 80 } 81 82 // check if we have valid unsaved data after end-of-file has reached 83 if currentPackage != noPackage && currentVersion != noVersion && !parsedPackages.Has(currentPackage+"@"+currentVersion) { 84 pkgs = append(pkgs, newYarnLockPackage(resolver, reader.Location, currentPackage, currentVersion)) 85 parsedPackages.Add(currentPackage + "@" + currentVersion) 86 } 87 88 if err := scanner.Err(); err != nil { 89 return nil, nil, fmt.Errorf("failed to parse yarn.lock file: %w", err) 90 } 91 92 pkg.Sort(pkgs) 93 94 return pkgs, nil, nil 95 } 96 97 func findPackageName(line string) string { 98 if matches := packageNameExp.FindStringSubmatch(line); len(matches) >= 2 { 99 return matches[1] 100 } 101 102 return noPackage 103 } 104 105 func findPackageVersion(line string) string { 106 if matches := versionExp.FindStringSubmatch(line); len(matches) >= 2 { 107 return matches[1] 108 } 109 110 return noVersion 111 } 112 113 func findPackageAndVersion(line string) (string, string) { 114 if matches := packageURLExp.FindStringSubmatch(line); len(matches) >= 2 { 115 return matches[1], matches[2] 116 } 117 118 return noPackage, noVersion 119 }