mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat(nodejs): add bun.lock parser (#8851)
Signed-off-by: Ashwani Kumar Kamal (sneaky-potato) <ashwanikamal.im421@gmail.com> Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
c321fdfcdd
commit
1dcf81666f
176
pkg/dependency/parser/nodejs/bun/parse.go
Normal file
176
pkg/dependency/parser/nodejs/bun/parse.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package bun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/dependency"
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
"github.com/aquasecurity/trivy/pkg/set"
|
||||
xio "github.com/aquasecurity/trivy/pkg/x/io"
|
||||
xjson "github.com/aquasecurity/trivy/pkg/x/json"
|
||||
)
|
||||
|
||||
type LockFile struct {
|
||||
Packages map[string]ParsedPackage `json:"packages"`
|
||||
Workspaces map[string]Workspace `json:"workspaces"`
|
||||
LockfileVersion int `json:"lockfileVersion"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
DevDependencies map[string]string `json:"devDependencies"`
|
||||
OptionalDependencies map[string]string `json:"optionalDependencies"`
|
||||
PeerDependencies map[string]string `json:"peerDependencies"`
|
||||
}
|
||||
|
||||
type ParsedPackage struct {
|
||||
Identifier string
|
||||
Meta map[string]any
|
||||
xjson.Location
|
||||
}
|
||||
|
||||
type Parser struct{}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{}
|
||||
}
|
||||
|
||||
func (p *ParsedPackage) UnmarshalJSON(data []byte) error {
|
||||
var raw []jsontext.Value
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return fmt.Errorf("expected package format: %w", err)
|
||||
}
|
||||
if len(raw) < 1 {
|
||||
return fmt.Errorf("invalid package entry: not enough elements: %s", string(data))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(raw[0], &p.Identifier); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(raw) > 2 {
|
||||
if err := json.Unmarshal(raw[2], &p.Meta); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
|
||||
var lockFile LockFile
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("file read error: %w", err)
|
||||
}
|
||||
if err = xjson.UnmarshalJSONC(data, &lockFile); err != nil {
|
||||
return nil, nil, xerrors.Errorf("JSON decode error: %w", err)
|
||||
}
|
||||
|
||||
pkgs := make(map[string]ftypes.Package, len(lockFile.Packages))
|
||||
deps := make(map[string][]string)
|
||||
|
||||
prodDirectDeps := set.New[string]()
|
||||
devDirectDeps := set.New[string]()
|
||||
|
||||
for _, ws := range lockFile.Workspaces {
|
||||
prodDirectDeps.Append(lo.Keys(ws.Dependencies)...)
|
||||
prodDirectDeps.Append(lo.Keys(ws.PeerDependencies)...)
|
||||
prodDirectDeps.Append(lo.Keys(ws.OptionalDependencies)...)
|
||||
devDirectDeps.Append(lo.Keys(ws.DevDependencies)...)
|
||||
}
|
||||
for pkgName, parsed := range lockFile.Packages {
|
||||
pkgVersion := strings.TrimPrefix(parsed.Identifier, pkgName+"@")
|
||||
if strings.HasPrefix(pkgVersion, "workspace") {
|
||||
pkgVersion = lockFile.Workspaces[pkgName].Version
|
||||
}
|
||||
pkgId := packageID(pkgName, pkgVersion)
|
||||
isDirect := prodDirectDeps.Contains(pkgName) || devDirectDeps.Contains(pkgName)
|
||||
|
||||
relationship := ftypes.RelationshipIndirect
|
||||
if _, ok := lockFile.Workspaces[pkgName]; ok {
|
||||
relationship = ftypes.RelationshipWorkspace
|
||||
} else if isDirect {
|
||||
relationship = ftypes.RelationshipDirect
|
||||
}
|
||||
|
||||
newPkg := ftypes.Package{
|
||||
ID: pkgId,
|
||||
Name: pkgName,
|
||||
Version: pkgVersion,
|
||||
Relationship: relationship,
|
||||
Dev: true, // Mark all dependencies as Dev. We will handle them later.
|
||||
Locations: []ftypes.Location{ftypes.Location(parsed.Location)},
|
||||
}
|
||||
pkgs[pkgName] = newPkg
|
||||
|
||||
var dependsOn []string
|
||||
if depMap, ok := parsed.Meta["dependencies"].(map[string]any); ok {
|
||||
dependsOn = lo.Keys(depMap)
|
||||
}
|
||||
|
||||
if len(dependsOn) > 0 {
|
||||
sort.Strings(dependsOn)
|
||||
deps[pkgName] = dependsOn
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
// Workspaces are always prod deps.
|
||||
if pkg.Relationship == ftypes.RelationshipWorkspace {
|
||||
pkg.Dev = false
|
||||
pkgs[pkg.Name] = pkg
|
||||
continue
|
||||
}
|
||||
if pkg.Relationship != ftypes.RelationshipDirect || !prodDirectDeps.Contains(pkg.Name) {
|
||||
continue
|
||||
}
|
||||
walkProdPackages(pkg.Name, pkgs, deps, set.New[string]())
|
||||
}
|
||||
|
||||
depSlice := lo.MapToSlice(deps, func(depName string, dependsOn []string) ftypes.Dependency {
|
||||
id := pkgs[depName]
|
||||
dependsOnIDs := make([]string, 0, len(dependsOn))
|
||||
for _, d := range dependsOn {
|
||||
dependsOnIDs = append(dependsOnIDs, pkgs[d].ID)
|
||||
}
|
||||
return ftypes.Dependency{
|
||||
ID: id.ID,
|
||||
DependsOn: dependsOnIDs,
|
||||
}
|
||||
})
|
||||
pkgSlice := lo.Values(pkgs)
|
||||
sort.Sort(ftypes.Packages(pkgSlice))
|
||||
sort.Sort(ftypes.Dependencies(depSlice))
|
||||
return pkgSlice, depSlice, nil
|
||||
}
|
||||
|
||||
// walkProdPackages marks all packages in the dependency tree of the given package as prod packages (Dev == false).
|
||||
func walkProdPackages(pkgName string, pkgs map[string]ftypes.Package, deps map[string][]string, visited set.Set[string]) {
|
||||
if visited.Contains(pkgName) {
|
||||
return
|
||||
}
|
||||
|
||||
// Disable Dev field for prod pkgs.
|
||||
pkg := pkgs[pkgName]
|
||||
pkg.Dev = false
|
||||
pkgs[pkgName] = pkg
|
||||
|
||||
visited.Append(pkgName)
|
||||
for _, dep := range deps[pkgName] {
|
||||
walkProdPackages(dep, pkgs, deps, visited)
|
||||
}
|
||||
}
|
||||
|
||||
func packageID(name, version string) string {
|
||||
return dependency.ID(ftypes.Bun, name, version)
|
||||
}
|
||||
57
pkg/dependency/parser/nodejs/bun/parse_test.go
Normal file
57
pkg/dependency/parser/nodejs/bun/parse_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package bun
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
file string // Test input file
|
||||
want []ftypes.Package
|
||||
wantDeps []ftypes.Dependency
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
file: "testdata/bun_happy.lock",
|
||||
want: normalPkgs,
|
||||
wantDeps: normalDeps,
|
||||
},
|
||||
{
|
||||
name: "invalid lockfile",
|
||||
file: "testdata/bun_invalid.lock",
|
||||
wantErr: "JSON decode error",
|
||||
},
|
||||
{
|
||||
name: "multiple workspaces",
|
||||
file: "testdata/bun_multiple_ws.lock",
|
||||
want: multipleWsPkgs,
|
||||
wantDeps: multipleWsDeps,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := os.Open(tt.file)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
got, deps, err := NewParser().Parse(f)
|
||||
if tt.wantErr != "" {
|
||||
assert.ErrorContains(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
if tt.wantDeps != nil {
|
||||
assert.Equal(t, tt.wantDeps, deps)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
160
pkg/dependency/parser/nodejs/bun/parse_testcase.go
Normal file
160
pkg/dependency/parser/nodejs/bun/parse_testcase.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package bun
|
||||
|
||||
import (
|
||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
normalPkgs = []ftypes.Package{
|
||||
{
|
||||
ID: "@types/bun@1.2.13",
|
||||
Name: "@types/bun",
|
||||
Version: "1.2.13",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Dev: true,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 18,
|
||||
EndLine: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "typescript@5.8.3",
|
||||
Name: "typescript",
|
||||
Version: "5.8.3",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Dev: false,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 24,
|
||||
EndLine: 24,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "zod@3.24.4",
|
||||
Name: "zod",
|
||||
Version: "3.24.4",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Dev: false,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 28,
|
||||
EndLine: 28,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "@types/node@22.15.18",
|
||||
Name: "@types/node",
|
||||
Version: "22.15.18",
|
||||
Relationship: ftypes.RelationshipIndirect,
|
||||
Dev: true,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 20,
|
||||
EndLine: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "bun-types@1.2.13",
|
||||
Name: "bun-types",
|
||||
Version: "1.2.13",
|
||||
Relationship: ftypes.RelationshipIndirect,
|
||||
Dev: true,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 22,
|
||||
EndLine: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "undici-types@6.21.0",
|
||||
Name: "undici-types",
|
||||
Version: "6.21.0",
|
||||
Relationship: ftypes.RelationshipIndirect,
|
||||
Dev: false,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 26,
|
||||
EndLine: 26,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
multipleWsPkgs = []ftypes.Package{
|
||||
{
|
||||
ID: "my-app@1.0.0",
|
||||
Name: "my-app",
|
||||
Version: "1.0.0",
|
||||
Relationship: ftypes.RelationshipWorkspace,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 27,
|
||||
EndLine: 27,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "my-lib@1.0.0",
|
||||
Name: "my-lib",
|
||||
Version: "1.0.0",
|
||||
Relationship: ftypes.RelationshipWorkspace,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 29,
|
||||
EndLine: 29,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "chalk@5.0.1",
|
||||
Name: "chalk",
|
||||
Version: "5.0.1",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 23,
|
||||
EndLine: 23,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "lodash@4.17.21",
|
||||
Name: "lodash",
|
||||
Version: "4.17.21",
|
||||
Relationship: ftypes.RelationshipDirect,
|
||||
Locations: ftypes.Locations{
|
||||
{
|
||||
StartLine: 25,
|
||||
EndLine: 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
normalDeps = []ftypes.Dependency{
|
||||
{
|
||||
ID: "@types/bun@1.2.13",
|
||||
DependsOn: []string{"bun-types@1.2.13"},
|
||||
},
|
||||
{
|
||||
ID: "@types/node@22.15.18",
|
||||
DependsOn: []string{"undici-types@6.21.0"},
|
||||
},
|
||||
{
|
||||
ID: "bun-types@1.2.13",
|
||||
DependsOn: []string{"@types/node@22.15.18"},
|
||||
},
|
||||
{
|
||||
ID: "zod@3.24.4",
|
||||
DependsOn: []string{"undici-types@6.21.0"},
|
||||
},
|
||||
}
|
||||
|
||||
multipleWsDeps = []ftypes.Dependency(nil)
|
||||
)
|
||||
30
pkg/dependency/parser/nodejs/bun/testdata/bun_happy.lock
vendored
Normal file
30
pkg/dependency/parser/nodejs/bun/testdata/bun_happy.lock
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "buntest",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"zod": "^3.24.4",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="],
|
||||
|
||||
"@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"zod": ["zod@3.24.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
|
||||
}
|
||||
}
|
||||
3
pkg/dependency/parser/nodejs/bun/testdata/bun_invalid.lock
vendored
Normal file
3
pkg/dependency/parser/nodejs/bun/testdata/bun_invalid.lock
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"lockfileVersion": "not-a-number",
|
||||
}
|
||||
31
pkg/dependency/parser/nodejs/bun/testdata/bun_multiple_ws.lock
vendored
Normal file
31
pkg/dependency/parser/nodejs/bun/testdata/bun_multiple_ws.lock
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "buntest",
|
||||
},
|
||||
"my-app": {
|
||||
"name": "my-app",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"lodash": "4.17.21",
|
||||
},
|
||||
},
|
||||
"my-lib": {
|
||||
"name": "my-lib",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"chalk": "5.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"chalk": ["chalk@5.0.1", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"my-app": ["my-app@workspace:my-app"],
|
||||
|
||||
"my-lib": ["my-lib@workspace:my-lib"],
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ const (
|
||||
Composer LangType = "composer"
|
||||
ComposerVendor LangType = "composer-vendor"
|
||||
Npm LangType = "npm"
|
||||
Bun LangType = "bun"
|
||||
NuGet LangType = "nuget"
|
||||
DotNetCore LangType = "dotnet-core"
|
||||
PackagesProps LangType = "packages-props"
|
||||
|
||||
Reference in New Issue
Block a user