From 30fe53e629d0b368cf0c5ff79ac9387c19b12f79 Mon Sep 17 00:00:00 2001 From: Yoav Alon <65133955+yoav-orca@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:21:49 +0300 Subject: [PATCH] fix(javascript): accept scalar people fields in package.json (#4779) Signed-off-by: Yoav Alon --- .../javascript/parse_package_json.go | 49 ++++---------- .../javascript/parse_package_json_test.go | 65 +++++++++++++++++++ .../pkg-json/package-authors-string.json | 12 ++++ .../pkg-json/package-contributors-string.json | 12 ++++ .../pkg-json/package-maintainers-object.json | 15 +++++ 5 files changed, 117 insertions(+), 36 deletions(-) create mode 100644 syft/pkg/cataloger/javascript/testdata/pkg-json/package-authors-string.json create mode 100644 syft/pkg/cataloger/javascript/testdata/pkg-json/package-contributors-string.json create mode 100644 syft/pkg/cataloger/javascript/testdata/pkg-json/package-maintainers-object.json diff --git a/syft/pkg/cataloger/javascript/parse_package_json.go b/syft/pkg/cataloger/javascript/parse_package_json.go index 582791dd3..758332e7d 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json.go +++ b/syft/pkg/cataloger/javascript/parse_package_json.go @@ -215,46 +215,23 @@ func pathContainsNodeModulesDirectory(p string) bool { } 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 + // Accept either a JSON array of authors, or a single author as a string or + // object — the latter is used in the wild (e.g. ghost@5.98.1) and dropping + // the whole package.json on those was https://github.com/anchore/syft/issues/4778. + var elements []json.RawMessage + if err := json.Unmarshal(b, &elements); err != nil { + // not an array — treat the whole payload as a single element + elements = []json.RawMessage{b} } - // Try to unmarshal as an array of objects - var authorObjs []map[string]any - 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 + auths := make([]person, len(elements)) + for i, e := range elements { + if err := json.Unmarshal(e, &auths[i]); err != nil { + return fmt.Errorf("unable to parse package.json author: %w", err) } - *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") + *p = auths + return nil } func (p people) String() string { diff --git a/syft/pkg/cataloger/javascript/parse_package_json_test.go b/syft/pkg/cataloger/javascript/parse_package_json_test.go index dd05c0b99..0b1c2a4ee 100644 --- a/syft/pkg/cataloger/javascript/parse_package_json_test.go +++ b/syft/pkg/cataloger/javascript/parse_package_json_test.go @@ -307,6 +307,71 @@ func TestParsePackageJSON(t *testing.T) { }, }, }, + { + // see https://github.com/anchore/syft/issues/4778 + // Ghost CMS publishes package.json with contributors as a single string + Fixture: "testdata/pkg-json/package-contributors-string.json", + ExpectedPkg: pkg.Package{ + Name: "ghost", + Version: "5.98.1", + PURL: "pkg:npm/ghost@5.98.1", + Type: pkg.NpmPkg, + Licenses: pkg.NewLicenseSet( + pkg.NewLicenseFromLocationsWithContext(ctx, "Artistic-2.0", file.NewLocation("testdata/pkg-json/package-contributors-string.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "ghost", + Version: "5.98.1", + Author: "https://github.com/TryGhost/Ghost/graphs/contributors", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, + { + Fixture: "testdata/pkg-json/package-authors-string.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("testdata/pkg-json/package-authors-string.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Harry Potter (http://youknowwho.com/)", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, + { + Fixture: "testdata/pkg-json/package-maintainers-object.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("testdata/pkg-json/package-maintainers-object.json")), + ), + Language: pkg.JavaScript, + Metadata: pkg.NpmPackage{ + Name: "npm", + Version: "6.14.6", + Author: "Charlie Maintainer ", + Homepage: "https://docs.npmjs.com/", + URL: "https://github.com/npm/cli", + Description: "a package manager for JavaScript", + }, + }, + }, { Fixture: "testdata/pkg-json/package-all-author-fields.json", ExpectedPkg: pkg.Package{ diff --git a/syft/pkg/cataloger/javascript/testdata/pkg-json/package-authors-string.json b/syft/pkg/cataloger/javascript/testdata/pkg-json/package-authors-string.json new file mode 100644 index 000000000..0cc5ac7ba --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/pkg-json/package-authors-string.json @@ -0,0 +1,12 @@ +{ + "version": "6.14.6", + "name": "npm", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "authors": "Harry Potter (http://youknowwho.com/)", + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} diff --git a/syft/pkg/cataloger/javascript/testdata/pkg-json/package-contributors-string.json b/syft/pkg/cataloger/javascript/testdata/pkg-json/package-contributors-string.json new file mode 100644 index 000000000..2b492a5f1 --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/pkg-json/package-contributors-string.json @@ -0,0 +1,12 @@ +{ + "version": "5.98.1", + "name": "ghost", + "description": "a package manager for JavaScript", + "homepage": "https://docs.npmjs.com/", + "contributors": "https://github.com/TryGhost/Ghost/graphs/contributors", + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +} diff --git a/syft/pkg/cataloger/javascript/testdata/pkg-json/package-maintainers-object.json b/syft/pkg/cataloger/javascript/testdata/pkg-json/package-maintainers-object.json new file mode 100644 index 000000000..7e8496ce2 --- /dev/null +++ b/syft/pkg/cataloger/javascript/testdata/pkg-json/package-maintainers-object.json @@ -0,0 +1,15 @@ +{ + "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" + }, + "repository": { + "type": "git", + "url": "https://github.com/npm/cli" + }, + "license": "Artistic-2.0" +}