fix(javascript): accept scalar people fields in package.json (#4779)

Signed-off-by: Yoav Alon <yoav@orca.security>
This commit is contained in:
Yoav Alon 2026-04-15 21:21:49 +03:00 committed by GitHub
parent 952469f0f0
commit 30fe53e629
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 117 additions and 36 deletions

View File

@ -215,46 +215,23 @@ func pathContainsNodeModulesDirectory(p string) bool {
} }
func (p *people) UnmarshalJSON(b []byte) error { func (p *people) UnmarshalJSON(b []byte) error {
// Try to unmarshal as an array of strings // Accept either a JSON array of authors, or a single author as a string or
var authorStrings []string // object — the latter is used in the wild (e.g. ghost@5.98.1) and dropping
if err := json.Unmarshal(b, &authorStrings); err == nil { // the whole package.json on those was https://github.com/anchore/syft/issues/4778.
// Successfully parsed as an array of strings var elements []json.RawMessage
auths := make([]person, len(authorStrings)) if err := json.Unmarshal(b, &elements); err != nil {
for i, authorStr := range authorStrings { // not an array — treat the whole payload as a single element
// Parse each string into author fields elements = []json.RawMessage{b}
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 != "" { auths := make([]person, len(elements))
auth.Name = strings.TrimSpace(auth.Name) 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)
} }
auths[i] = auth
} }
*p = auths *p = auths
return nil return nil
}
// 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
}
*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 { func (p people) String() string {

View File

@ -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 <hp@hogwards.com> (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 <charlie@example.com>",
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", Fixture: "testdata/pkg-json/package-all-author-fields.json",
ExpectedPkg: pkg.Package{ ExpectedPkg: pkg.Package{

View File

@ -0,0 +1,12 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"authors": "Harry Potter <hp@hogwards.com> (http://youknowwho.com/)",
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}

View File

@ -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"
}

View File

@ -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"
}