diff --git a/pkg/dependency/parser/nodejs/packagejson/parse.go b/pkg/dependency/parser/nodejs/packagejson/parse.go index f4bf258f7aae..19a53679f2d0 100644 --- a/pkg/dependency/parser/nodejs/packagejson/parse.go +++ b/pkg/dependency/parser/nodejs/packagejson/parse.go @@ -5,6 +5,7 @@ import ( "io" "regexp" + "github.com/samber/lo" "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/dependency" @@ -21,7 +22,7 @@ type packageJSON struct { Dependencies map[string]string `json:"dependencies"` OptionalDependencies map[string]string `json:"optionalDependencies"` DevDependencies map[string]string `json:"devDependencies"` - Workspaces []string `json:"workspaces"` + Workspaces any `json:"workspaces"` } type Package struct { @@ -65,7 +66,7 @@ func (p *Parser) Parse(r io.Reader) (Package, error) { Dependencies: pkgJSON.Dependencies, OptionalDependencies: pkgJSON.OptionalDependencies, DevDependencies: pkgJSON.DevDependencies, - Workspaces: pkgJSON.Workspaces, + Workspaces: parseWorkspaces(pkgJSON.Workspaces), }, nil } @@ -82,6 +83,29 @@ func parseLicense(val interface{}) string { return "" } +// parseWorkspaces returns slice of workspaces +func parseWorkspaces(val any) []string { + // Workspaces support 2 types - https://github.com/SchemaStore/schemastore/blob/d9516961f8a5b0e65a457808070147b5a866f60b/src/schemas/json/package.json#L777 + switch ws := val.(type) { + // Workspace as object (map[string][]string) + // e.g. "workspaces": {"packages": ["packages/*", "plugins/*"]}, + case map[string]interface{}: + // Take only workspaces for `packages` - https://classic.yarnpkg.com/blog/2018/02/15/nohoist/ + if pkgsWorkspaces, ok := ws["packages"]; ok { + return lo.Map(pkgsWorkspaces.([]interface{}), func(workspace interface{}, _ int) string { + return workspace.(string) + }) + } + // Workspace as string array + // e.g. "workspaces": ["packages/*", "backend"], + case []interface{}: + return lo.Map(ws, func(workspace interface{}, _ int) string { + return workspace.(string) + }) + } + return nil +} + func IsValidName(name string) bool { // Name is optional field // https://docs.npmjs.com/cli/v9/configuring-npm/package-json#name diff --git a/pkg/dependency/parser/nodejs/packagejson/parse_test.go b/pkg/dependency/parser/nodejs/packagejson/parse_test.go index 4f04cebcc1ee..97a0027d22ef 100644 --- a/pkg/dependency/parser/nodejs/packagejson/parse_test.go +++ b/pkg/dependency/parser/nodejs/packagejson/parse_test.go @@ -76,6 +76,20 @@ func TestParse(t *testing.T) { }, }, }, + { + name: "happy path - workspace as struct", + inputFile: "testdata/workspace_as_map_package.json", + want: packagejson.Package{ + Library: types.Library{ + ID: "example@1.0.0", + Name: "example", + Version: "1.0.0", + }, + Workspaces: []string{ + "packages/*", + }, + }, + }, { name: "invalid package name", inputFile: "testdata/invalid_name.json", diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/workspace_as_map_package.json b/pkg/dependency/parser/nodejs/packagejson/testdata/workspace_as_map_package.json new file mode 100644 index 000000000000..21a198e8bc6e --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/workspace_as_map_package.json @@ -0,0 +1,8 @@ +{ + "name": "example", + "version": "1.0.0", + "workspaces": { + "packages": ["packages/*"], + "nohoist": ["**/react-native", "**/react-native/**"] + } +}