refactor: migrate from github.com/aquasecurity/jfather to github.com/go-json-experiment/json (#8591)

This commit is contained in:
DmitriyLewen
2025-04-09 18:22:57 +06:00
committed by GitHub
parent 9792611b36
commit 4b84dabd15
15 changed files with 336 additions and 279 deletions

2
go.mod
View File

@@ -48,7 +48,7 @@ require (
github.com/docker/go-units v0.5.0
github.com/fatih/color v1.18.0
github.com/go-git/go-git/v5 v5.14.0
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // Replace with encoding/json/v2 when proposal is accepted. Track https://github.com/golang/go/issues/71497
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // Replace with encoding/json/v2 when proposal is accepted. Track https://github.com/golang/go/issues/71497
github.com/go-openapi/runtime v0.28.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-redis/redis/v8 v8.11.5

4
go.sum
View File

@@ -1127,8 +1127,8 @@ github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY=
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=

View File

@@ -1,18 +1,19 @@
package conan
import (
"io"
"slices"
"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/jfather"
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
type LockFile struct {
@@ -27,17 +28,18 @@ type GraphLock struct {
type Node struct {
Ref string `json:"ref"`
Requires []string `json:"requires"`
StartLine int
EndLine int
xjson.Location
}
type Requires []Require
type Require struct {
Dependency string
StartLine int
EndLine int
xjson.Location
}
type Requires []Require
func (r *Require) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
return json.UnmarshalDecode(dec, &r.Dependency)
}
type Parser struct {
logger *log.Logger
@@ -63,7 +65,7 @@ func (p *Parser) parseV1(lock LockFile) ([]ftypes.Package, []ftypes.Dependency,
if node.Ref == "" {
continue
}
pkg, err := toPackage(node.Ref, node.StartLine, node.EndLine)
pkg, err := toPackage(node.Ref, node.Location)
if err != nil {
p.logger.Debug("Parse ref error", log.Err(err))
continue
@@ -105,7 +107,7 @@ func (p *Parser) parseV2(lock LockFile) ([]ftypes.Package, []ftypes.Dependency,
var pkgs []ftypes.Package
for _, req := range lock.Requires {
pkg, err := toPackage(req.Dependency, req.StartLine, req.EndLine)
pkg, err := toPackage(req.Dependency, req.Location)
if err != nil {
p.logger.Debug("Creating package entry from requirement failed", log.Err(err))
continue
@@ -118,12 +120,7 @@ func (p *Parser) parseV2(lock LockFile) ([]ftypes.Package, []ftypes.Dependency,
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lock LockFile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("failed to read conan lock file: %w", err)
}
if err := jfather.Unmarshal(input, &lock); err != nil {
if err := xjson.UnmarshalRead(r, &lock); err != nil {
return nil, nil, xerrors.Errorf("failed to decode conan lock file: %w", err)
}
@@ -152,7 +149,7 @@ func parsePackage(text string) (string, string, error) {
return ss[0], ss[1], nil
}
func toPackage(pkg string, startLine, endLine int) (ftypes.Package, error) {
func toPackage(pkg string, location xjson.Location) (ftypes.Package, error) {
name, version, err := parsePackage(pkg)
if err != nil {
return ftypes.Package{}, err
@@ -161,33 +158,6 @@ func toPackage(pkg string, startLine, endLine int) (ftypes.Package, error) {
ID: dependency.ID(ftypes.Conan, name, version),
Name: name,
Version: version,
Locations: []ftypes.Location{
{
StartLine: startLine,
EndLine: endLine,
},
},
Locations: []ftypes.Location{ftypes.Location(location)},
}, nil
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps
func (n *Node) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&n); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
n.StartLine = node.Range().Start.Line
n.EndLine = node.Range().End.Line
return nil
}
func (r *Require) UnmarshalJSONWithMetadata(node jfather.Node) error {
var dep string
if err := node.Decode(&dep); err != nil {
return err
}
r.Dependency = dep
r.StartLine = node.Range().Start.Line
r.EndLine = node.Range().End.Line
return nil
}

View File

@@ -1,7 +1,6 @@
package core_deps
import (
"io"
"sort"
"strings"
"sync"
@@ -9,11 +8,11 @@ import (
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/jfather"
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
type dotNetDependencies struct {
@@ -24,8 +23,7 @@ type dotNetDependencies struct {
type dotNetLibrary struct {
Type string `json:"type"`
StartLine int
EndLine int
xjson.Location
}
type RuntimeTarget struct {
@@ -52,12 +50,7 @@ func NewParser() *Parser {
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var depsFile dotNetDependencies
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("read error: %w", err)
}
if err = jfather.Unmarshal(input, &depsFile); err != nil {
if err := xjson.UnmarshalRead(r, &depsFile); err != nil {
return nil, nil, xerrors.Errorf("failed to decode .deps.json file: %w", err)
}
@@ -90,12 +83,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
ID: dependency.ID(ftypes.DotNetCore, split[0], split[1]),
Name: split[0],
Version: split[1],
Locations: []ftypes.Location{
{
StartLine: lib.StartLine,
EndLine: lib.EndLine,
},
},
Locations: []ftypes.Location{ftypes.Location(lib.Location)},
})
}
@@ -118,14 +106,3 @@ func (p *Parser) isRuntimeLibrary(targetLibs map[string]TargetLib, library strin
// Check that `runtime`, `runtimeTarget` and `native` sections are not empty
return !lo.IsEmpty(lib)
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps
func (t *dotNetLibrary) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&t); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
t.StartLine = node.Range().Start.Line
t.EndLine = node.Range().End.Line
return nil
}

View File

@@ -82,7 +82,7 @@ func TestParse(t *testing.T) {
{
name: "sad path",
file: "testdata/invalid.deps.json",
wantErr: "failed to decode .deps.json file: EOF",
wantErr: "failed to decode .deps.json file: jsontext: unexpected EOF within",
},
}

View File

@@ -2,7 +2,6 @@ package npm
import (
"fmt"
"io"
"maps"
"path"
"slices"
@@ -12,13 +11,13 @@ import (
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/jfather"
"github.com/aquasecurity/trivy/pkg/dependency"
"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/set"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
const nodeModulesDir = "node_modules"
@@ -34,8 +33,7 @@ type Dependency struct {
Dependencies map[string]Dependency `json:"dependencies"`
Requires map[string]string `json:"requires"`
Resolved string `json:"resolved"`
StartLine int
EndLine int
xjson.Location
}
type Package struct {
@@ -49,8 +47,7 @@ type Package struct {
Dev bool `json:"dev"`
Link bool `json:"link"`
Workspaces []string `json:"workspaces"`
StartLine int
EndLine int
xjson.Location
}
type Parser struct {
@@ -65,11 +62,7 @@ func NewParser() *Parser {
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockFile LockFile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("read error: %w", err)
}
if err := jfather.Unmarshal(input, &lockFile); err != nil {
if err := xjson.UnmarshalRead(r, &lockFile); err != nil {
return nil, nil, xerrors.Errorf("decode error: %w", err)
}
@@ -117,10 +110,6 @@ func (p *Parser) parseV2(packages map[string]Package) ([]ftypes.Package, []ftype
}
pkgID := packageID(pkgName, pkg.Version)
location := ftypes.Location{
StartLine: pkg.StartLine,
EndLine: pkg.EndLine,
}
var ref ftypes.ExternalRef
if pkg.Resolved != "" {
@@ -145,7 +134,7 @@ func (p *Parser) parseV2(packages map[string]Package) ([]ftypes.Package, []ftype
sortExternalReferences(savedPkg.ExternalReferences)
}
savedPkg.Locations = append(savedPkg.Locations, location)
savedPkg.Locations = append(savedPkg.Locations, ftypes.Location(pkg.Location))
sort.Sort(savedPkg.Locations)
pkgs[pkgID] = savedPkg
@@ -159,7 +148,7 @@ func (p *Parser) parseV2(packages map[string]Package) ([]ftypes.Package, []ftype
Relationship: lo.Ternary(pkgIndirect, ftypes.RelationshipIndirect, ftypes.RelationshipDirect),
Dev: pkg.Dev,
ExternalReferences: lo.Ternary(ref.URL != "", []ftypes.ExternalRef{ref}, nil),
Locations: []ftypes.Location{location},
Locations: []ftypes.Location{ftypes.Location(pkg.Location)},
}
pkgs[pkgID] = newPkg
@@ -304,12 +293,7 @@ func (p *Parser) parseV1(dependencies map[string]Dependency, versions map[string
URL: dep.Resolved,
},
},
Locations: []ftypes.Location{
{
StartLine: dep.StartLine,
EndLine: dep.EndLine,
},
},
Locations: []ftypes.Location{ftypes.Location(dep.Location)},
}
pkgs = append(pkgs, pkg)
@@ -396,28 +380,6 @@ func joinPaths(paths ...string) string {
return strings.Join(paths, "/")
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps for v1
func (t *Dependency) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&t); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
t.StartLine = node.Range().Start.Line
t.EndLine = node.Range().End.Line
return nil
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps for v2 or newer
func (t *Package) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&t); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
t.StartLine = node.Range().Start.Line
t.EndLine = node.Range().End.Line
return nil
}
func packageID(name, version string) string {
return dependency.ID(ftypes.Npm, name, version)
}

View File

@@ -1,16 +1,14 @@
package lock
import (
"io"
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/jfather"
"github.com/aquasecurity/trivy/pkg/dependency"
"github.com/aquasecurity/trivy/pkg/dependency/parser/utils"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
type LockFile struct {
@@ -23,9 +21,8 @@ type Dependencies map[string]Dependency
type Dependency struct {
Type string `json:"type"`
Resolved string `json:"resolved"`
StartLine int
EndLine int
Dependencies map[string]string `json:"dependencies,omitempty"`
xjson.Location
}
type Parser struct{}
@@ -36,11 +33,7 @@ func NewParser() *Parser {
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockFile LockFile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("failed to read packages.lock.json: %w", err)
}
if err := jfather.Unmarshal(input, &lockFile); err != nil {
if err := xjson.UnmarshalRead(r, &lockFile); err != nil {
return nil, nil, xerrors.Errorf("failed to decode packages.lock.json: %w", err)
}
@@ -60,12 +53,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
Name: packageName,
Version: packageContent.Resolved,
Relationship: lo.Ternary(packageContent.Type == "Direct", ftypes.RelationshipDirect, ftypes.RelationshipIndirect),
Locations: []ftypes.Location{
{
StartLine: packageContent.StartLine,
EndLine: packageContent.EndLine,
},
},
Locations: []ftypes.Location{ftypes.Location(packageContent.Location)},
}
pkgs = append(pkgs, pkg)
@@ -97,17 +85,6 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
return utils.UniquePackages(pkgs), deps, nil
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps
func (t *Dependency) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&t); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
t.StartLine = node.Range().Start.Line
t.EndLine = node.Range().End.Line
return nil
}
func packageID(name, version string) string {
return dependency.ID(ftypes.NuGet, name, version)
}

View File

@@ -1,19 +1,18 @@
package composer
import (
"io"
"sort"
"strings"
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/jfather"
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/licensing"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
type LockFile struct {
@@ -24,8 +23,7 @@ type packageInfo struct {
Version string `json:"version"`
Require map[string]string `json:"require"`
License any `json:"license"`
StartLine int
EndLine int
xjson.Location
}
type Parser struct {
@@ -40,11 +38,7 @@ func NewParser() *Parser {
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockFile LockFile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("read error: %w", err)
}
if err = jfather.Unmarshal(input, &lockFile); err != nil {
if err := xjson.UnmarshalRead(r, &lockFile); err != nil {
return nil, nil, xerrors.Errorf("decode error: %w", err)
}
@@ -57,12 +51,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
Version: lpkg.Version,
Relationship: ftypes.RelationshipUnknown, // composer.lock file doesn't have info about direct/indirect dependencies
Licenses: licenses(lpkg.License),
Locations: []ftypes.Location{
{
StartLine: lpkg.StartLine,
EndLine: lpkg.EndLine,
},
},
Locations: []ftypes.Location{ftypes.Location(lpkg.Location)},
}
pkgs[pkg.Name] = pkg
@@ -105,17 +94,6 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
return pkgSlice, deps, nil
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps
func (t *packageInfo) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&t); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
t.StartLine = node.Range().Start.Line
t.EndLine = node.Range().End.Line
return nil
}
// licenses returns slice of licenses from string, string with separators (`or`, `and`, etc.) or string array
// cf. https://getcomposer.org/doc/04-schema.md#license
func licenses(val any) []string {

View File

@@ -1,14 +1,13 @@
package pipenv
import (
"io"
"strings"
"golang.org/x/xerrors"
"github.com/aquasecurity/jfather"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
type lockFile struct {
@@ -16,8 +15,7 @@ type lockFile struct {
}
type dependency struct {
Version string `json:"version"`
StartLine int
EndLine int
xjson.Location
}
type Parser struct{}
@@ -28,11 +26,7 @@ func NewParser() *Parser {
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockFile lockFile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("failed to read packages.lock.json: %w", err)
}
if err := jfather.Unmarshal(input, &lockFile); err != nil {
if err := xjson.UnmarshalRead(r, &lockFile); err != nil {
return nil, nil, xerrors.Errorf("failed to decode Pipenv.lock: %w", err)
}
@@ -41,24 +35,8 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
pkgs = append(pkgs, ftypes.Package{
Name: pkgName,
Version: strings.TrimLeft(dep.Version, "="),
Locations: []ftypes.Location{
{
StartLine: dep.StartLine,
EndLine: dep.EndLine,
},
},
Locations: []ftypes.Location{ftypes.Location(dep.Location)},
})
}
return pkgs, nil, nil
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps
func (t *dependency) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&t); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
t.StartLine = node.Range().Start.Line
t.EndLine = node.Range().End.Line
return nil
}

View File

@@ -1,16 +1,15 @@
package lockfile
import (
"io"
"slices"
"sort"
"golang.org/x/xerrors"
"github.com/aquasecurity/jfather"
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
// lockfile format defined at: https://stringbean.github.io/sbt-dependency-lock/file-formats/version-1.html
@@ -24,8 +23,7 @@ type sbtLockfileDependency struct {
Name string `json:"name"`
Version string `json:"version"`
Configurations []string `json:"configurations"`
StartLine int
EndLine int
xjson.Location
}
type Parser struct{}
@@ -36,12 +34,7 @@ func NewParser() *Parser {
func (Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockfile sbtLockfile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("failed to read sbt lockfile: %w", err)
}
if err := jfather.Unmarshal(input, &lockfile); err != nil {
if err := xjson.UnmarshalRead(r, &lockfile); err != nil {
return nil, nil, xerrors.Errorf("JSON decoding failed: %w", err)
}
@@ -54,12 +47,7 @@ func (Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency,
ID: dependency.ID(ftypes.Sbt, name, dep.Version),
Name: name,
Version: dep.Version,
Locations: []ftypes.Location{
{
StartLine: dep.StartLine,
EndLine: dep.EndLine,
},
},
Locations: []ftypes.Location{ftypes.Location(dep.Location)},
})
}
}
@@ -68,17 +56,6 @@ func (Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency,
return libraries, nil, nil
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps
func (t *sbtLockfileDependency) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&t); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
t.StartLine = node.Range().Start.Line
t.EndLine = node.Range().End.Line
return nil
}
func isIncludedConfig(config string) bool {
return config == "compile" || config == "runtime"
}

View File

@@ -1,18 +1,17 @@
package swift
import (
"io"
"sort"
"strings"
"github.com/samber/lo"
"golang.org/x/xerrors"
"github.com/aquasecurity/jfather"
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
// Parser is a parser for Package.resolved files
@@ -28,11 +27,7 @@ func NewParser() *Parser {
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var lockFile LockFile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("read error: %w", err)
}
if err := jfather.Unmarshal(input, &lockFile); err != nil {
if err := xjson.UnmarshalRead(r, &lockFile); err != nil {
return nil, nil, xerrors.Errorf("decode error: %w", err)
}
@@ -58,12 +53,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
ID: dependency.ID(ftypes.Swift, name, version),
Name: name,
Version: version,
Locations: []ftypes.Location{
{
StartLine: pin.StartLine,
EndLine: pin.EndLine,
},
},
Locations: []ftypes.Location{ftypes.Location(pin.Location)},
})
}
sort.Sort(pkgs)
@@ -75,7 +65,7 @@ func pkgName(pin Pin, lockVersion int) string {
// v2 uses `Location`
name := pin.RepositoryURL
if lockVersion > 1 {
name = pin.Location
name = pin.Loc
}
// Swift uses `https://github.com/<author>/<package>.git format
// `.git` suffix can be omitted (take a look happy test)
@@ -84,14 +74,3 @@ func pkgName(pin Pin, lockVersion int) string {
name = strings.TrimSuffix(name, ".git")
return name
}
// UnmarshalJSONWithMetadata needed to detect start and end lines of deps for v1
func (p *Pin) UnmarshalJSONWithMetadata(node jfather.Node) error {
if err := node.Decode(&p); err != nil {
return err
}
// Decode func will overwrite line numbers if we save them first
p.StartLine = node.Range().Start.Line
p.EndLine = node.Range().End.Line
return nil
}

View File

@@ -1,5 +1,9 @@
package swift
import (
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
type LockFile struct {
Object Object `json:"object"`
Pins []Pin `json:"pins"`
@@ -13,10 +17,9 @@ type Object struct {
type Pin struct {
Package string `json:"package"`
RepositoryURL string `json:"repositoryURL"` // Package.revision v1
Location string `json:"location"` // Package.revision v2
Loc string `json:"location"` // Package.revision v2
State State `json:"state"`
StartLine int
EndLine int
xjson.Location
}
type State struct {

View File

@@ -44,7 +44,7 @@ func ManifestFromJSON(path string, data []byte) (*Manifest, error) {
}
if err := json.Unmarshal(data, root, json.WithUnmarshalers(
json.UnmarshalFromFunc(func(dec *jsontext.Decoder, node *ManifestNode, opts json.Options) error {
json.UnmarshalFromFunc(func(dec *jsontext.Decoder, node *ManifestNode) error {
startOffset := dec.InputOffset()
if err := unmarshalManifestNode(dec, node); err != nil {
return err

107
pkg/x/json/json.go Normal file
View File

@@ -0,0 +1,107 @@
package json
import (
"bytes"
"io"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/set"
)
// lineReader is a custom reader that tracks line numbers.
type lineReader struct {
r io.Reader
line int
}
// newLineReader creates a new line reader.
func newLineReader(r io.Reader) *lineReader {
return &lineReader{
r: r,
line: 1,
}
}
func (lr *lineReader) Read(p []byte) (n int, err error) {
n, err = lr.r.Read(p)
if n > 0 {
// Count the number of newlines in the read buffer
lr.line += bytes.Count(p[:n], []byte("\n"))
}
return n, err
}
func (lr *lineReader) Line() int {
return lr.line
}
func Unmarshal(data []byte, v any) error {
return UnmarshalRead(bytes.NewBuffer(data), v)
}
func UnmarshalRead(r io.Reader, v any) error {
lr := newLineReader(r)
unmarshalers := unmarshalerWithObjectLocation(lr)
return json.UnmarshalRead(lr, v, json.WithUnmarshalers(unmarshalers))
}
// Location is wrap of types.Location.
// This struct is required when you need to detect location of your object from json file.
type Location types.Location
func (l *Location) SetLocation(location types.Location) {
*l = Location(location)
}
// ObjectLocation is required when you need to save Location for your struct.
type ObjectLocation interface {
SetLocation(location types.Location)
}
// unmarshalerWithObjectLocation creates json.Unmarshaler for ObjectLocation to save object location into xjson.Location
// To use UnmarshalerWithObjectLocation for primitive types, you must implement the UnmarshalerFrom interface for those objects.
// cf. https://pkg.go.dev/github.com/go-json-experiment/json#UnmarshalerFrom
func unmarshalerWithObjectLocation(r *lineReader) *json.Unmarshalers {
visited := set.New[int]()
return unmarshaler(r, visited)
}
func unmarshaler(r *lineReader, visited set.Set[int]) *json.Unmarshalers {
return json.UnmarshalFromFunc(func(dec *jsontext.Decoder, loc ObjectLocation) error {
// Decoder.InputOffset reports the offset after the last token,
// but we want to record the offset before the next token.
//
// Call Decoder.PeekKind to buffer enough to reach the next token.
// Add the number of leading whitespace, commas, and colons
// to locate the start of the next token.
// cf. https://pkg.go.dev/github.com/go-json-experiment/json@v0.0.0-20250223041408-d3c622f1b874#example-WithUnmarshalers-RecordOffsets
kind := dec.PeekKind()
unread := bytes.TrimLeft(dec.UnreadBuffer(), " \n\r\t,:")
start := r.Line() - bytes.Count(unread, []byte("\n")) // The decoder buffer may have read more lines.
// Check visited set to avoid infinity loops
if visited.Contains(start) {
return json.SkipFunc
}
visited.Append(start)
// Return more detailed error for cases when UnmarshalJSONFrom is not implemented for primitive type.
if _, ok := loc.(json.UnmarshalerFrom); !ok && kind != '[' && kind != '{' {
return xerrors.Errorf("structures with single primitive type should implement UnmarshalJSONFrom: %T", loc)
}
if err := json.UnmarshalDecode(dec, loc, json.WithUnmarshalers(unmarshaler(r, visited))); err != nil {
return err
}
loc.SetLocation(types.Location{
StartLine: start,
EndLine: r.Line() - bytes.Count(dec.UnreadBuffer(), []byte("\n")),
})
return nil
})
}

149
pkg/x/json/json_test.go Normal file
View File

@@ -0,0 +1,149 @@
package json_test
import (
"testing"
"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/stretchr/testify/require"
xjson "github.com/aquasecurity/trivy/pkg/x/json"
)
// See npm.LockFile
type nestedStruct struct {
Dependencies map[string]Dependency `json:"dependencies"`
}
type Dependency struct {
Version string `json:"version"`
Dependencies map[string]Dependency `json:"dependencies"`
xjson.Location
}
type stringWithLocation struct {
Requires Requires `json:"requires"`
}
type Requires []Require
type Require struct {
Dependency string
xjson.Location
}
func (r *Require) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
return json.UnmarshalDecode(dec, &r.Dependency)
}
type stringsWithoutUnmarshalerFrom struct {
Strings []StringWithoutUnmarshalerFrom `json:"strings"`
}
type StringWithoutUnmarshalerFrom struct {
String string
xjson.Location
}
func TestUnmarshal(t *testing.T) {
tests := []struct {
name string
in []byte
out any
want any
wantErr string
}{
{
name: "nested LocationObjects",
in: []byte(`{
"dependencies": {
"body-parser": {
"version": "1.18.3",
"dependencies": {
"debug": {
"version": "2.6.9"
}
}
}
}
}`),
out: nestedStruct{},
want: nestedStruct{
Dependencies: map[string]Dependency{
"body-parser": {
Version: "1.18.3",
Location: xjson.Location{
StartLine: 3,
EndLine: 10,
},
Dependencies: map[string]Dependency{
// UnmarshalerWithObjectLocation doesn't support Location for nested objects
"debug": {
Version: "2.6.9",
Location: xjson.Location{
StartLine: 6,
EndLine: 8,
},
},
},
},
},
},
},
{
name: "Location for only string",
in: []byte(`{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245"
]
}`),
out: stringWithLocation{},
want: stringWithLocation{
Requires: []Require{
{
Dependency: "sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
Location: xjson.Location{
StartLine: 4,
EndLine: 4,
},
},
{
Dependency: "matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245",
Location: xjson.Location{
StartLine: 5,
EndLine: 5,
},
},
},
},
},
{
name: "String object without UnmarshalerFrom implementation",
in: []byte(`{
"strings": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245"
]
}`),
out: stringsWithoutUnmarshalerFrom{},
wantErr: "structures with single primitive type should implement UnmarshalJSONFrom",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := xjson.Unmarshal(tt.in, &tt.out)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, tt.out)
})
}
}