diff --git a/cmd/cmd.go b/cmd/cmd.go index 5d93443bb..f59b08689 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -101,7 +101,7 @@ func initLogging() { logWrapper := logger.NewLogrusLogger(cfg) syft.SetLogger(logWrapper) stereoscope.SetLogger(&logger.LogrusNestedLogger{ - Logger: logWrapper.Logger.WithField("from-lib", "steroscope"), + Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"), }) } diff --git a/syft/cataloger/javascript/parse_package_json.go b/syft/cataloger/javascript/parse_package_json.go index 654f1b68b..1a8db0a51 100644 --- a/syft/cataloger/javascript/parse_package_json.go +++ b/syft/cataloger/javascript/parse_package_json.go @@ -2,6 +2,7 @@ package javascript import ( "encoding/json" + "errors" "fmt" "io" "regexp" @@ -22,7 +23,8 @@ type PackageJSON struct { Version string `json:"version"` Latest []string `json:"latest"` Author Author `json:"author"` - License string `json:"license"` + License json.RawMessage `json:"license"` + Licenses []license `json:"licenses,omitempty"` Name string `json:"name"` Homepage string `json:"homepage"` Description string `json:"description"` @@ -107,6 +109,56 @@ func (r *Repository) UnmarshalJSON(b []byte) error { return nil } +type license struct { + Type string `json:"type"` + URL string `json:"url"` +} + +func licenseFromJSON(b []byte) (string, error) { + // first try as string + var licenseString string + err := json.Unmarshal(b, &licenseString) + if err == nil { + return licenseString, nil + } + + // then try as object (this format is deprecated) + var licenseObject license + err = json.Unmarshal(b, &licenseObject) + if err == nil { + return licenseObject.Type, nil + } + + return "", errors.New("unable to unmarshal license field as either string or object") +} + +func licensesFromJSON(p PackageJSON) ([]string, error) { + if p.License == nil && p.Licenses == nil { + // This package.json doesn't specify any licenses whatsoever + return []string{}, nil + } + + singleLicense, err := licenseFromJSON(p.License) + if err == nil { + return []string{singleLicense}, nil + } + + // The "licenses" field is deprecated. It should be inspected as a last resort. + if p.Licenses != nil { + mapLicenses := func(licenses []license) []string { + mappedLicenses := make([]string, len(licenses)) + for i, l := range licenses { + mappedLicenses[i] = l.Type + } + return mappedLicenses + } + + return mapLicenses(p.Licenses), nil + } + + return nil, fmt.Errorf("unable to parse license field: %w", err) +} + // parsePackageJson parses a package.json and returns the discovered JavaScript packages. func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) { packages := make([]pkg.Package, 0) @@ -120,10 +172,15 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) { return nil, fmt.Errorf("failed to parse package.json file: %w", err) } + licenses, err := licensesFromJSON(p) + if err != nil { + return nil, fmt.Errorf("failed to parse package.json file: %w", err) + } + packages = append(packages, pkg.Package{ Name: p.Name, Version: p.Version, - Licenses: []string{p.License}, + Licenses: licenses, Language: pkg.JavaScript, Type: pkg.NpmPkg, MetadataType: pkg.NpmPackageJSONMetadataType, @@ -131,7 +188,7 @@ func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) { Author: p.Author.AuthorString(), Homepage: p.Homepage, URL: p.Repository.URL, - Licenses: []string{p.License}, + Licenses: licenses, }, }) } diff --git a/syft/cataloger/javascript/parse_package_json_test.go b/syft/cataloger/javascript/parse_package_json_test.go index 869d4293f..c2940a7a2 100644 --- a/syft/cataloger/javascript/parse_package_json_test.go +++ b/syft/cataloger/javascript/parse_package_json_test.go @@ -30,6 +30,57 @@ func TestParsePackageJSON(t *testing.T) { }, }, }, + { + Fixture: "test-fixtures/pkg-json/package-license-object.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + Type: pkg.NpmPkg, + Licenses: []string{"ISC"}, + Language: pkg.JavaScript, + MetadataType: pkg.NpmPackageJSONMetadataType, + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "Isaac Z. Schlueter (http://blog.izs.me)", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Licenses: []string{"ISC"}, + }, + }, + }, + { + Fixture: "test-fixtures/pkg-json/package-license-objects.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + Type: pkg.NpmPkg, + Licenses: []string{"MIT", "Apache-2.0"}, + Language: pkg.JavaScript, + MetadataType: pkg.NpmPackageJSONMetadataType, + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "Isaac Z. Schlueter (http://blog.izs.me)", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Licenses: []string{"MIT", "Apache-2.0"}, + }, + }, + }, + { + Fixture: "test-fixtures/pkg-json/package-no-license.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + Type: pkg.NpmPkg, + Licenses: []string{}, + Language: pkg.JavaScript, + MetadataType: pkg.NpmPackageJSONMetadataType, + Metadata: pkg.NpmPackageJSONMetadata{ + Author: "Isaac Z. Schlueter (http://blog.izs.me)", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Licenses: []string{}, + }, + }, + }, { Fixture: "test-fixtures/pkg-json/package-nested-author.json", ExpectedPkg: pkg.Package{ @@ -73,7 +124,7 @@ func TestParsePackageJSON(t *testing.T) { t.Fatalf("failed to open fixture: %+v", err) } - actual, err := parsePackageJSON(fixture.Name(), fixture) + actual, err := parsePackageJSON("", fixture) if err != nil { t.Fatalf("failed to parse package-lock.json: %+v", err) } diff --git a/syft/cataloger/javascript/test-fixtures/pkg-json/package-license-object.json b/syft/cataloger/javascript/test-fixtures/pkg-json/package-license-object.json new file mode 100644 index 000000000..9c5d61567 --- /dev/null +++ b/syft/cataloger/javascript/test-fixtures/pkg-json/package-license-object.json @@ -0,0 +1,22 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "author": "Isaac Z. Schlueter (http://blog.izs.me)", + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "bugs": { + "url": "https://npm.community/c/bugs" + }, + "main": "./lib/npm.js", + "license": { + "type" : "ISC", + "url" : "https://opensource.org/licenses/ISC" + }, + "engines": { + "node": "6 >=6.2.0 || 8 || >=9.3.0" + } +} diff --git a/syft/cataloger/javascript/test-fixtures/pkg-json/package-license-objects.json b/syft/cataloger/javascript/test-fixtures/pkg-json/package-license-objects.json new file mode 100644 index 000000000..605bc3ec9 --- /dev/null +++ b/syft/cataloger/javascript/test-fixtures/pkg-json/package-license-objects.json @@ -0,0 +1,26 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "author": "Isaac Z. Schlueter (http://blog.izs.me)", + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "bugs": { + "url": "https://npm.community/c/bugs" + }, + "main": "./lib/npm.js", + "licenses": [ + { "type": "MIT", + "url": "https://www.opensource.org/licenses/mit-license.php" + }, + { "type": "Apache-2.0", + "url": "https://opensource.org/licenses/apache2.0.php" + } + ], + "engines": { + "node": "6 >=6.2.0 || 8 || >=9.3.0" + } +} diff --git a/syft/cataloger/javascript/test-fixtures/pkg-json/package-no-license.json b/syft/cataloger/javascript/test-fixtures/pkg-json/package-no-license.json new file mode 100644 index 000000000..9c1b29e24 --- /dev/null +++ b/syft/cataloger/javascript/test-fixtures/pkg-json/package-no-license.json @@ -0,0 +1,18 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "author": "Isaac Z. Schlueter (http://blog.izs.me)", + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "bugs": { + "url": "https://npm.community/c/bugs" + }, + "main": "./lib/npm.js", + "engines": { + "node": "6 >=6.2.0 || 8 || >=9.3.0" + } +}