mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
Co-authored-by: knqyf263 <knqyf263@users.noreply.github.com> Co-authored-by: nikpivkin <nikita.pivkin@smartforce.io>
188 lines
5.5 KiB
Go
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)
|
|
}
|