From 9f314eb493fd45d9a806a5ccdf0acd3688f9db72 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Wed, 22 Jul 2020 14:21:44 -0400 Subject: [PATCH] cataloger: support npm packages from package-lock.json files Signed-off-by: Alfredo Deza --- imgbom/cataloger/npm/cataloger.go | 34 +++++ imgbom/cataloger/npm/parse_package_lock.go | 48 +++++++ .../cataloger/npm/parse_package_lock_test.go | 119 ++++++++++++++++++ .../test-fixtures/pkg-lock/package-lock.json | 73 +++++++++++ 4 files changed, 274 insertions(+) create mode 100644 imgbom/cataloger/npm/cataloger.go create mode 100644 imgbom/cataloger/npm/parse_package_lock.go create mode 100644 imgbom/cataloger/npm/parse_package_lock_test.go create mode 100644 imgbom/cataloger/npm/test-fixtures/pkg-lock/package-lock.json diff --git a/imgbom/cataloger/npm/cataloger.go b/imgbom/cataloger/npm/cataloger.go new file mode 100644 index 000000000..d3ebc7647 --- /dev/null +++ b/imgbom/cataloger/npm/cataloger.go @@ -0,0 +1,34 @@ +package npm + +import ( + "github.com/anchore/imgbom/imgbom/cataloger/common" + "github.com/anchore/imgbom/imgbom/pkg" + "github.com/anchore/imgbom/imgbom/scope" + "github.com/anchore/stereoscope/pkg/file" +) + +type Cataloger struct { + cataloger common.GenericCataloger +} + +func NewCataloger() *Cataloger { + globParsers := map[string]common.ParserFn{ + "**/package-lock.json": parsePackageLock, + } + + return &Cataloger{ + cataloger: common.NewGenericCataloger(nil, globParsers), + } +} + +func (a *Cataloger) Name() string { + return "npm-cataloger" +} + +func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { + return a.cataloger.SelectFiles(resolver) +} + +func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { + return a.cataloger.Catalog(contents, a.Name()) +} diff --git a/imgbom/cataloger/npm/parse_package_lock.go b/imgbom/cataloger/npm/parse_package_lock.go new file mode 100644 index 000000000..4181cfa25 --- /dev/null +++ b/imgbom/cataloger/npm/parse_package_lock.go @@ -0,0 +1,48 @@ +package npm + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/anchore/imgbom/imgbom/pkg" +) + +type PackageLock struct { + Requires bool `json:"requires"` + LockfileVersion int `json:"lockfileVersion"` + Dependencies Dependencies +} + +type Dependency struct { + Version string `json:"version"` + Resolved string `json:"resolved"` + Integrity string `json:"integrity"` + Requires map[string]string +} + +type Dependencies map[string]Dependency + +func parsePackageLock(_ string, reader io.Reader) ([]pkg.Package, error) { + packages := make([]pkg.Package, 0) + dec := json.NewDecoder(reader) + + for { + var lock PackageLock + if err := dec.Decode(&lock); err == io.EOF { + break + } else if err != nil { + return nil, fmt.Errorf("failed to parse package-lock.json file: %w", err) + } + for name, pkgMeta := range lock.Dependencies { + packages = append(packages, pkg.Package{ + Name: name, + Version: pkgMeta.Version, + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }) + } + } + + return packages, nil +} diff --git a/imgbom/cataloger/npm/parse_package_lock_test.go b/imgbom/cataloger/npm/parse_package_lock_test.go new file mode 100644 index 000000000..1ad1bb7a2 --- /dev/null +++ b/imgbom/cataloger/npm/parse_package_lock_test.go @@ -0,0 +1,119 @@ +package npm + +import ( + "os" + "testing" + + "github.com/anchore/imgbom/imgbom/pkg" +) + +func assertPkgsEqual(t *testing.T, actual []pkg.Package, expected map[string]pkg.Package) { + t.Helper() + if len(actual) != len(expected) { + for _, a := range actual { + t.Log(" ", a) + } + t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expected)) + } + + for _, a := range actual { + expectedPkg, ok := expected[a.Name] + if !ok { + t.Errorf("unexpected package found: '%s'", a.Name) + } + + if expectedPkg.Version != a.Version { + t.Errorf("%s : unexpected package version: '%s', expected: '%s'", a.Name, a.Version, expectedPkg.Version) + } + + if a.Language != expectedPkg.Language { + t.Errorf("%s : bad language: '%+v', expected: '%+v'", a.Name, a.Language, expectedPkg.Language) + } + + if a.Type != expectedPkg.Type { + t.Errorf("%s : bad package type: %+v, expected: %+v", a.Name, a.Type, expectedPkg.Type) + } + + if len(a.Licenses) < len(expectedPkg.Licenses) { + t.Errorf("%s : bad package licenses count: '%+v'", a.Name, a.Licenses) + } + + } +} + +func TestParsePackageLock(t *testing.T) { + expected := map[string]pkg.Package{ + "wordwrap": { + Name: "wordwrap", + Version: "0.0.3", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "get-stdin": { + Name: "get-stdin", + Version: "5.0.1", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "minimist": { + Name: "minimist", + Version: "0.0.10", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "optimist": { + Name: "optimist", + Version: "0.6.1", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "string-width": { + Name: "string-width", + Version: "2.1.1", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "strip-ansi": { + Name: "strip-ansi", + Version: "4.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "strip-eof": { + Name: "wordwrap", + Version: "1.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "ansi-regex": { + Name: "ansi-regex", + Version: "3.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "is-fullwidth-code-point": { + Name: "is-fullwidth-code-point", + Version: "2.0.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + "cowsay": { + Name: "cowsay", + Version: "1.4.0", + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + } + fixture, err := os.Open("test-fixtures/pkg-lock/package-lock.json") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + actual, err := parsePackageLock(fixture.Name(), fixture) + if err != nil { + t.Fatalf("failed to parse package-lock.json: %+v", err) + } + + assertPkgsEqual(t, actual, expected) + +} diff --git a/imgbom/cataloger/npm/test-fixtures/pkg-lock/package-lock.json b/imgbom/cataloger/npm/test-fixtures/pkg-lock/package-lock.json new file mode 100644 index 000000000..4056cac02 --- /dev/null +++ b/imgbom/cataloger/npm/test-fixtures/pkg-lock/package-lock.json @@ -0,0 +1,73 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "cowsay": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.4.0.tgz", + "integrity": "sha512-rdg5k5PsHFVJheO/pmE3aDg2rUDDTfPJau6yYkZYlHFktUz+UxbE+IgnUAEyyCyv4noL5ltxXD0gZzmHPCy/9g==", + "requires": { + "get-stdin": "^5.0.1", + "optimist": "~0.6.1", + "string-width": "~2.1.1", + "strip-eof": "^1.0.0" + } + }, + "get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + } + } +}