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