Files
trivy/pkg/dependency/parser/nodejs/bun/parse.go
2025-09-15 08:46:44 +00:00

188 lines
5.5 KiB
Go

package bun
import (
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"io"
"sort"
"strings"
"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
}
// When package contains only package field [pkg: string]
// cf. https://github.com/oven-sh/bun/blob/61e03a275885b9b48f7a28f6dfbbbe1156eedca6/packages/bun-types/bun.d.ts#L7751
if len(raw) == 1 {
return nil
}
// Meta can be 2 or 3 array elements
// [pkg: string, info: BunLockFilePackageInfo]
// [pkg: string, info: BunLockFilePackageInfo, bunTag: string]
// [pkg: string, info: Pick<BunLockFileBasePackageInfo, "bin" | "binDir">]
// [pkg: string, registry: string, info: BunLockFilePackageInfo, integrity: string]
// cf.https://github.com/oven-sh/bun/blob/61e03a275885b9b48f7a28f6dfbbbe1156eedca6/packages/bun-types/bun.d.ts#L7745-L7755
metaRaw := raw[1]
if len(raw) > 3 {
metaRaw = raw[2]
}
return json.Unmarshal(metaRaw, &p.Meta)
}
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)
}