diff --git a/syft/pkg/cataloger/javascript/package.go b/syft/pkg/cataloger/javascript/package.go index ed901fc80..f74a5da9a 100644 --- a/syft/pkg/cataloger/javascript/package.go +++ b/syft/pkg/cataloger/javascript/package.go @@ -25,6 +25,39 @@ func newPackageJSONPackage(ctx context.Context, u packageJSON, indexLocation fil } license := pkg.NewLicensesFromLocationWithContext(ctx, indexLocation, licenseCandidates...) + // Handle author, authors, contributors, and maintainers fields + var authorParts []string + + // Add a single author field if it exists + if u.Author.Name != "" || u.Author.Email != "" || u.Author.URL != "" { + if authStr := u.Author.AuthorString(); authStr != "" { + authorParts = append(authorParts, authStr) + } + } + + // Add authors field if it exists + if len(u.Authors) > 0 { + if authorsStr := u.Authors.String(); authorsStr != "" { + authorParts = append(authorParts, authorsStr) + } + } + + // Add contributors field if it exists + if len(u.Contributors) > 0 { + if contributorsStr := u.Contributors.String(); contributorsStr != "" { + authorParts = append(authorParts, contributorsStr) + } + } + + // Add maintainers field if it exists + if len(u.Maintainers) > 0 { + if maintainersStr := u.Maintainers.String(); maintainersStr != "" { + authorParts = append(authorParts, maintainersStr) + } + } + + authorInfo := strings.Join(authorParts, ", ") + p := pkg.Package{ Name: u.Name, Version: u.Version, @@ -37,7 +70,7 @@ func newPackageJSONPackage(ctx context.Context, u packageJSON, indexLocation fil Name: u.Name, Version: u.Version, Description: u.Description, - Author: u.Author.AuthorString(), + Author: authorInfo, Homepage: u.Homepage, URL: u.Repository.URL, Private: u.Private, diff --git a/syft/pkg/cataloger/javascript/parse_package_json.go b/syft/pkg/cataloger/javascript/parse_package_json.go index 5da2c5412..3a34871a1 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json.go +++ b/syft/pkg/cataloger/javascript/parse_package_json.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "regexp" + "strings" "github.com/go-viper/mapstructure/v2" @@ -24,7 +25,10 @@ var _ generic.Parser = parsePackageJSON type packageJSON struct { Version string `json:"version"` Latest []string `json:"latest"` - Author author `json:"author"` + Author person `json:"author"` + Authors people `json:"authors"` + Contributors people `json:"contributors"` + Maintainers people `json:"maintainers"` License json.RawMessage `json:"license"` Licenses json.RawMessage `json:"licenses"` Name string `json:"name"` @@ -35,12 +39,14 @@ type packageJSON struct { Private bool `json:"private"` } -type author struct { +type person struct { Name string `json:"name" mapstructure:"name"` Email string `json:"email" mapstructure:"email"` URL string `json:"url" mapstructure:"url"` } +type people []person + type repository struct { Type string `json:"type" mapstructure:"type"` URL string `json:"url" mapstructure:"url"` @@ -76,9 +82,9 @@ func parsePackageJSON(ctx context.Context, _ file.Resolver, _ *generic.Environme return pkgs, nil, nil } -func (a *author) UnmarshalJSON(b []byte) error { +func (p *person) UnmarshalJSON(b []byte) error { var authorStr string - var auth author + var auth person if err := json.Unmarshal(b, &authorStr); err == nil { // successfully parsed as a string, now parse that string into fields @@ -97,18 +103,18 @@ func (a *author) UnmarshalJSON(b []byte) error { } } - *a = auth + *p = auth return nil } -func (a *author) AuthorString() string { - result := a.Name - if a.Email != "" { - result += fmt.Sprintf(" <%s>", a.Email) +func (p *person) AuthorString() string { + result := p.Name + if p.Email != "" { + result += fmt.Sprintf(" <%s>", p.Email) } - if a.URL != "" { - result += fmt.Sprintf(" (%s)", a.URL) + if p.URL != "" { + result += fmt.Sprintf(" (%s)", p.URL) } return result } @@ -210,3 +216,58 @@ func pathContainsNodeModulesDirectory(p string) bool { } return false } + +func (p *people) UnmarshalJSON(b []byte) error { + // Try to unmarshal as an array of strings + var authorStrings []string + if err := json.Unmarshal(b, &authorStrings); err == nil { + // Successfully parsed as an array of strings + auths := make([]person, len(authorStrings)) + for i, authorStr := range authorStrings { + // Parse each string into author fields + fields := internal.MatchNamedCaptureGroups(authorPattern, authorStr) + var auth person + if err := mapstructure.Decode(fields, &auth); err != nil { + return fmt.Errorf("unable to decode package.json author: %w", err) + } + // Trim whitespace from name if it was parsed + if auth.Name != "" { + auth.Name = strings.TrimSpace(auth.Name) + } + auths[i] = auth + } + *p = auths + return nil + } + + // Try to unmarshal as an array of objects + var authorObjs []map[string]interface{} + if err := json.Unmarshal(b, &authorObjs); err == nil { + // Successfully parsed as an array of objects + auths := make([]person, len(authorObjs)) + for i, fields := range authorObjs { + var auth person + if err := mapstructure.Decode(fields, &auth); err != nil { + return fmt.Errorf("unable to decode package.json author object: %w", err) + } + auths[i] = auth + } + *p = auths + return nil + } + + // If we get here, it means neither format matched + return fmt.Errorf("unable to parse package.json authors field: expected array of strings or array of objects") +} + +func (p people) String() string { + if len(p) == 0 { + return "" + } + + authorStrings := make([]string, len(p)) + for i, auth := range p { + authorStrings[i] = auth.AuthorString() + } + return strings.Join(authorStrings, ", ") +} diff --git a/syft/pkg/cataloger/javascript/parse_package_json_test.go b/syft/pkg/cataloger/javascript/parse_package_json_test.go index 7764cde5f..01da032a9 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_json_test.go @@ -152,7 +152,7 @@ func TestParsePackageJSON(t *testing.T) { Metadata: pkg.NpmPackage{ Name: "function-bind", Version: "1.1.1", - Author: "Raynos ", + Author: "Raynos , Raynos, Jordan Harband (https://github.com/ljharb)", Homepage: "https://github.com/Raynos/function-bind", URL: "git://github.com/Raynos/function-bind.git", Description: "Implementation of Function.prototype.bind", @@ -202,6 +202,132 @@ func TestParsePackageJSON(t *testing.T) { }, }, }, + { + Fixture: "test-fixtures/pkg-json/package-authors-array.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-authors-array.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Harry Potter (http://youknowwho.com/), John Smith (http://awebsite.com/)", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, + { + Fixture: "test-fixtures/pkg-json/package-authors-objects.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-authors-objects.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Harry Potter (http://youknowwho.com/), John Smith (http://awebsite.com/)", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, + { + Fixture: "test-fixtures/pkg-json/package-both-author-and-authors.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-both-author-and-authors.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Isaac Z. Schlueter (http://blog.izs.me), Harry Potter (http://youknowwho.com/), John Smith (http://awebsite.com/)", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, + { + Fixture: "test-fixtures/pkg-json/package-contributors.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-contributors.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Alice Contributor , Bob Helper ", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, + { + Fixture: "test-fixtures/pkg-json/package-maintainers.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-maintainers.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Charlie Maintainer , Diana Keeper ", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, + { + Fixture: "test-fixtures/pkg-json/package-all-author-fields.json", + ExpectedPkg: pkg.Package{ + Name: "npm", + Version: "6.14.6", + PURL: "pkg:npm/npm@6.14.6", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("test-fixtures/pkg-json/package-all-author-fields.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Main Author , Second Author , Contrib One , Maintainer One ", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-all-author-fields.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-all-author-fields.json new file mode 100644 index 000000000..188e73f8b --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-all-author-fields.json @@ -0,0 +1,24 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "author": "Main Author ", + "authors": [ + "Second Author " + ], + "contributors": [ + "Contrib One " + ], + "maintainers": [ + { + "name": "Maintainer One", + "email": "maintain1@example.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} \ No newline at end of file diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-authors-array.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-authors-array.json new file mode 100644 index 000000000..bce357d54 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-authors-array.json @@ -0,0 +1,15 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "authors": [ + "Harry Potter (http://youknowwho.com/)", + "John Smith (http://awebsite.com/)" + ], + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-authors-objects.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-authors-objects.json new file mode 100644 index 000000000..335b099dd --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-authors-objects.json @@ -0,0 +1,23 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "authors": [ + { + "name": "Harry Potter", + "email": "hp@hogwards.com", + "url": "http://youknowwho.com/" + }, + { + "name": "John Smith", + "email": "j.smith@something.com", + "url": "http://awebsite.com/" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-both-author-and-authors.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-both-author-and-authors.json new file mode 100644 index 000000000..f6fec0694 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-both-author-and-authors.json @@ -0,0 +1,16 @@ +{ + "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)", + "authors": [ + "Harry Potter (http://youknowwho.com/)", + "John Smith (http://awebsite.com/)" + ], + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-contributors.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-contributors.json new file mode 100644 index 000000000..b1378afb6 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-contributors.json @@ -0,0 +1,15 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "contributors": [ + "Alice Contributor ", + "Bob Helper " + ], + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} \ No newline at end of file diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-maintainers.json b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-maintainers.json new file mode 100644 index 000000000..88cd5a6b7 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pkg-json/package-maintainers.json @@ -0,0 +1,21 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "maintainers": [ + { + "name": "Charlie Maintainer", + "email": "charlie@example.com" + }, + { + "name": "Diana Keeper", + "email": "diana@example.com" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} \ No newline at end of file