diff --git a/syft/cataloger/controller.go b/syft/cataloger/controller.go index 14c0ce735..b82318cb3 100644 --- a/syft/cataloger/controller.go +++ b/syft/cataloger/controller.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/syft/syft/cataloger/dpkg" golang "github.com/anchore/syft/syft/cataloger/golang" "github.com/anchore/syft/syft/cataloger/java" - "github.com/anchore/syft/syft/cataloger/npm" + "github.com/anchore/syft/syft/cataloger/javascript" "github.com/anchore/syft/syft/cataloger/python" "github.com/anchore/syft/syft/cataloger/rpmdb" "github.com/anchore/syft/syft/event" @@ -53,7 +53,7 @@ func newController() controller { ctrlr.add(java.NewCataloger()) ctrlr.add(apkdb.NewCataloger()) ctrlr.add(golang.NewCataloger()) - ctrlr.add(npm.NewCataloger()) + ctrlr.add(javascript.NewCataloger()) return ctrlr } diff --git a/syft/cataloger/npm/cataloger.go b/syft/cataloger/javascript/cataloger.go similarity index 92% rename from syft/cataloger/npm/cataloger.go rename to syft/cataloger/javascript/cataloger.go index f39a6db90..cd926b1be 100644 --- a/syft/cataloger/npm/cataloger.go +++ b/syft/cataloger/javascript/cataloger.go @@ -1,4 +1,4 @@ -package npm +package javascript import ( "github.com/anchore/stereoscope/pkg/file" @@ -14,6 +14,7 @@ type Cataloger struct { func NewCataloger() *Cataloger { globParsers := map[string]common.ParserFn{ "**/package-lock.json": parsePackageLock, + "**/yarn.lock": parseYarnLock, } return &Cataloger{ diff --git a/syft/cataloger/npm/parse_package_lock.go b/syft/cataloger/javascript/parse_package_lock.go similarity index 98% rename from syft/cataloger/npm/parse_package_lock.go rename to syft/cataloger/javascript/parse_package_lock.go index 7374530bd..9027abd6f 100644 --- a/syft/cataloger/npm/parse_package_lock.go +++ b/syft/cataloger/javascript/parse_package_lock.go @@ -1,4 +1,4 @@ -package npm +package javascript import ( "encoding/json" diff --git a/syft/cataloger/npm/parse_package_lock_test.go b/syft/cataloger/javascript/parse_package_lock_test.go similarity index 99% rename from syft/cataloger/npm/parse_package_lock_test.go rename to syft/cataloger/javascript/parse_package_lock_test.go index cff7bff4e..1f0b0a086 100644 --- a/syft/cataloger/npm/parse_package_lock_test.go +++ b/syft/cataloger/javascript/parse_package_lock_test.go @@ -1,4 +1,4 @@ -package npm +package javascript import ( "os" diff --git a/syft/cataloger/javascript/parse_yarn_lock.go b/syft/cataloger/javascript/parse_yarn_lock.go new file mode 100644 index 000000000..6e245c739 --- /dev/null +++ b/syft/cataloger/javascript/parse_yarn_lock.go @@ -0,0 +1,75 @@ +package javascript + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strings" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" +) + +var composedNameExp = regexp.MustCompile("^\"(@{1}[^@]+)") +var simpleNameExp = regexp.MustCompile(`^[a-zA-Z\-]+@`) +var versionExp = regexp.MustCompile(`^\W+(version)\W+`) + +func parseYarnLock(_ string, reader io.Reader) ([]pkg.Package, error) { + packages := make([]pkg.Package, 0) + fields := make(map[string]string) + var currentName string + + scanner := bufio.NewScanner(reader) + + 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] = "" + } + + switch { + case composedNameExp.MatchString(line): + name := composedNameExp.FindString(line) + if len(name) == 0 { + log.Errorf("unable to parse line: '%s'", 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 + 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.YarnPkg, + }) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to parse yarn.lock file: %w", err) + } + + return packages, nil +} diff --git a/syft/cataloger/javascript/parse_yarn_lock_test.go b/syft/cataloger/javascript/parse_yarn_lock_test.go new file mode 100644 index 000000000..3decbb86a --- /dev/null +++ b/syft/cataloger/javascript/parse_yarn_lock_test.go @@ -0,0 +1,67 @@ +package javascript + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" +) + +func TestParseYarnLock(t *testing.T) { + expected := map[string]pkg.Package{ + "@babel/code-frame": { + Name: "@babel/code-frame", + Version: "7.10.4", + Language: pkg.JavaScript, + Type: pkg.YarnPkg, + }, + "@types/minimatch": { + Name: "@types/minimatch", + Version: "3.0.3", + Language: pkg.JavaScript, + Type: pkg.YarnPkg, + }, + "@types/qs": { + Name: "@types/qs", + Version: "6.9.4", + Language: pkg.JavaScript, + Type: pkg.YarnPkg, + }, + "ajv": { + Name: "ajv", + Version: "6.12.3", + Language: pkg.JavaScript, + Type: pkg.YarnPkg, + }, + "atob": { + Name: "atob", + Version: "2.1.2", + Language: pkg.JavaScript, + Type: pkg.YarnPkg, + }, + "aws-sdk": { + Name: "aws-sdk", + Version: "2.706.0", + Language: pkg.JavaScript, + Type: pkg.YarnPkg, + }, + "jhipster-core": { + Name: "jhipster-core", + Version: "7.3.4", + Language: pkg.JavaScript, + Type: pkg.YarnPkg, + }, + } + fixture, err := os.Open("test-fixtures/yarn/yarn.lock") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + actual, err := parseYarnLock(fixture.Name(), fixture) + if err != nil { + t.Fatalf("failed to parse yarn.lock: %+v", err) + } + + assertPkgsEqual(t, actual, expected) + +} diff --git a/syft/cataloger/npm/test-fixtures/pkg-lock/package-lock.json b/syft/cataloger/javascript/test-fixtures/pkg-lock/package-lock.json similarity index 100% rename from syft/cataloger/npm/test-fixtures/pkg-lock/package-lock.json rename to syft/cataloger/javascript/test-fixtures/pkg-lock/package-lock.json diff --git a/syft/cataloger/javascript/test-fixtures/yarn/yarn.lock b/syft/cataloger/javascript/test-fixtures/yarn/yarn.lock new file mode 100644 index 000000000..0a9ab9366 --- /dev/null +++ b/syft/cataloger/javascript/test-fixtures/yarn/yarn.lock @@ -0,0 +1,71 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@types/minimatch@*", "@types/minimatch@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/qs@^6.2.31": + version "6.9.4" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" + integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + +"@types/qs@^6.2.31": + version "6.9.4" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" + integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + +ajv@^6.10.2, ajv@^6.5.5: + version "6.12.3" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sdk@2.706.0: + version "2.706.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953" + integrity sha512-7GT+yrB5Wb/zOReRdv/Pzkb2Qt+hz6B/8FGMVaoysX3NryHvQUdz7EQWi5yhg9CxOjKxdw5lFwYSs69YlSp1KA== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +jhipster-core@7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/jhipster-core/-/jhipster-core-7.3.4.tgz#c34b8c97c7f4e8b7518dae015517e2112c73cc80" + integrity sha512-AUhT69kNkqppaJZVfan/xnKG4Gs9Ggj7YLtTZFVe+xg+THrbMb5Ng7PL07PDlDw4KAEA33GMCwuAf65E8EpC4g== + dependencies: + chevrotain "7.0.1" + fs-extra "8.1.0" + lodash "4.17.15" + winston "3.2.1" + diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 9aa230073..bdf906e45 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -10,6 +10,7 @@ const ( RpmPkg WheelPkg NpmPkg + YarnPkg PythonRequirementsPkg JavaPkg JenkinsPluginPkg @@ -28,6 +29,7 @@ var typeStr = []string{ "rpm", "wheel", "npm", + "yarn", "python-requirements", "java-archive", "jenkins-plugin", @@ -43,6 +45,7 @@ var AllPkgs = []Type{ RpmPkg, WheelPkg, NpmPkg, + YarnPkg, PythonRequirementsPkg, JavaPkg, JenkinsPluginPkg, diff --git a/test/integration/fixture_pkg_coverage_test.go b/test/integration/fixture_pkg_coverage_test.go index 7b6524d42..4a59d4e0d 100644 --- a/test/integration/fixture_pkg_coverage_test.go +++ b/test/integration/fixture_pkg_coverage_test.go @@ -68,6 +68,14 @@ var cases = []struct { "get-stdin": "8.0.0", }, }, + { + name: "find javascript yarn packages", + pkgType: pkg.YarnPkg, + pkgLanguage: pkg.JavaScript, + pkgInfo: map[string]string{ + "@babel/code-frame": "7.10.4", + }, + }, { name: "find python egg packages", pkgType: pkg.EggPkg, diff --git a/test/integration/test-fixtures/image-pkg-coverage/javascript/yarn/yarn.lock b/test/integration/test-fixtures/image-pkg-coverage/javascript/yarn/yarn.lock new file mode 100644 index 000000000..a97627b6c --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/javascript/yarn/yarn.lock @@ -0,0 +1,12 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +