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] // [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) }