github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/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/kastenhq/syft/internal"
     9  	"github.com/kastenhq/syft/syft/artifact"
    10  	"github.com/kastenhq/syft/syft/file"
    11  	"github.com/kastenhq/syft/syft/pkg"
    12  	"github.com/kastenhq/syft/syft/pkg/cataloger/generic"
    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  }