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  }