diff --git a/syft/pkg/cataloger/javascript/parse_yarn_lock.go b/syft/pkg/cataloger/javascript/parse_yarn_lock.go index 078610d47..36e8ae4c0 100644 --- a/syft/pkg/cataloger/javascript/parse_yarn_lock.go +++ b/syft/pkg/cataloger/javascript/parse_yarn_lock.go @@ -5,9 +5,8 @@ import ( "fmt" "io" "regexp" - "strings" - "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/common" ) @@ -15,59 +14,53 @@ import ( // integrity check var _ common.ParserFn = parseYarnLock -var composedNameExp = regexp.MustCompile("^\"(@{1}[^@]+)") -var simpleNameExp = regexp.MustCompile(`^[a-zA-Z\-]+@`) -var versionExp = regexp.MustCompile(`^\W+(version)\W+`) +var ( + composedNameExp = regexp.MustCompile(`^"(@[^@]+)`) + simpleNameExp = regexp.MustCompile(`^(\w[\w-_.]*)@`) + versionExp = regexp.MustCompile(`^\W+version\W+"([\w-_.]+)"`) +) + +const ( + noPackage = "" + noVersion = "" +) func parseYarnLock(_ string, reader io.Reader) ([]pkg.Package, error) { - packages := make([]pkg.Package, 0) - fields := make(map[string]string) - var currentName string + var packages []pkg.Package scanner := bufio.NewScanner(reader) + parsedPackages := internal.NewStringSet() + currentPackage := noPackage for scanner.Scan() { line := scanner.Text() - line = strings.TrimRight(line, "\n") - // create the entry so that the loop can keep appending versions later - _, ok := fields[currentName] - if !ok { - fields[currentName] = "" - } + if currentPackage == noPackage { + // Scan until we find the next package - switch { - case composedNameExp.MatchString(line): - name := composedNameExp.FindString(line) - if len(name) == 0 { - log.Warnf("unable to parse yarn.lock line: %q", line) - } - currentName = strings.TrimLeft(name, "\"") - case simpleNameExp.MatchString(line): - parts := strings.Split(line, "@") - currentName = parts[0] - case versionExp.MatchString(line): - parts := strings.Split(line, " \"") - version := parts[len(parts)-1] - - versions, ok := fields[currentName] - if !ok { - return nil, fmt.Errorf("no previous key exists, expecting: %s", currentName) - } - - if strings.Contains(versions, version) { - // already exists from another dependency declaration + packageName := findPackageName(line) + if packageName == noPackage { continue } - // append the version as a string so that we can check on it later - fields[currentName] = versions + " " + version - packages = append(packages, pkg.Package{ - Name: currentName, - Version: strings.Trim(version, "\""), - Language: pkg.JavaScript, - Type: pkg.NpmPkg, - }) + if parsedPackages.Contains(packageName) { + // We don't parse repeated package declarations. + continue + } + + currentPackage = packageName + parsedPackages.Add(currentPackage) + + continue + } + + // We've found the package entry, now we just need the version + + if version := findPackageVersion(line); version != noVersion { + packages = append(packages, newYarnLockPackage(currentPackage, version)) + currentPackage = noPackage + + continue } } @@ -77,3 +70,32 @@ func parseYarnLock(_ string, reader io.Reader) ([]pkg.Package, error) { return packages, nil } + +func findPackageName(line string) string { + if matches := composedNameExp.FindStringSubmatch(line); len(matches) >= 2 { + return matches[1] + } + + if matches := simpleNameExp.FindStringSubmatch(line); len(matches) >= 2 { + return matches[1] + } + + return noPackage +} + +func findPackageVersion(line string) string { + if matches := versionExp.FindStringSubmatch(line); len(matches) >= 2 { + return matches[1] + } + + return noVersion +} + +func newYarnLockPackage(name, version string) pkg.Package { + return pkg.Package{ + Name: name, + Version: version, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + } +} diff --git a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go index 6ea75cb71..fd4ecd2e3 100644 --- a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go @@ -51,7 +51,20 @@ func TestParseYarnLock(t *testing.T) { Language: pkg.JavaScript, Type: pkg.NpmPkg, }, + "asn1.js": { + Name: "asn1.js", + Version: "4.10.1", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "c0n-fab_u.laTION": { + Name: "c0n-fab_u.laTION", + Version: "7.7.7", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, } + fixture, err := os.Open("test-fixtures/yarn/yarn.lock") if err != nil { t.Fatalf("failed to open fixture: %+v", err) @@ -63,5 +76,4 @@ func TestParseYarnLock(t *testing.T) { } assertPkgsEqual(t, actual, expected) - } diff --git a/syft/pkg/cataloger/javascript/test-fixtures/yarn/yarn.lock b/syft/pkg/cataloger/javascript/test-fixtures/yarn/yarn.lock index 0a9ab9366..f7ff8d510 100644 --- a/syft/pkg/cataloger/javascript/test-fixtures/yarn/yarn.lock +++ b/syft/pkg/cataloger/javascript/test-fixtures/yarn/yarn.lock @@ -69,3 +69,16 @@ jhipster-core@7.3.4: lodash "4.17.15" winston "3.2.1" +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +c0n-fab_u.laTION@^7.0.0: + version "7.7.7" + resolved "https://registry.yarnpkg.com/something-i-made-up/-/c0n-fab_u.laTION-7.7.7.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==