Files
trivy/pkg/dependency/parser/golang/mod/parse.go
Teppei Fukuda 983ac15f22 ci: add depguard (#6963)
Signed-off-by: knqyf263 <knqyf263@gmail.com>
2024-06-20 02:48:08 +00:00

174 lines
4.8 KiB
Go

package mod
import (
"io"
"regexp"
"strconv"
"strings"
"github.com/samber/lo"
"golang.org/x/mod/modfile"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/dependency"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
xio "github.com/aquasecurity/trivy/pkg/x/io"
)
var (
// By convention, modules with a major version equal to or above v2
// have it as suffix in their module path.
VCSUrlMajorVersionSuffixRegex = regexp.MustCompile(`(/v[\d]+)$`)
// gopkg.in/user/pkg.v -> github.com/user/pkg
VCSUrlGoPkgInRegexWithUser = regexp.MustCompile(`^gopkg\.in/([^/]+)/([^.]+)\..*$`)
// gopkg.in without user segment
// Example: gopkg.in/pkg.v3 -> github.com/go-pkg/pkg
VCSUrlGoPkgInRegexWithoutUser = regexp.MustCompile(`^gopkg\.in/([^.]+)\..*$`)
)
type Parser struct {
replace bool // 'replace' represents if the 'replace' directive should be taken into account.
}
func NewParser(replace bool) *Parser {
return &Parser{
replace: replace,
}
}
func (p *Parser) GetExternalRefs(path string) []ftypes.ExternalRef {
if url := resolveVCSUrl(path); url != "" {
return []ftypes.ExternalRef{
{
Type: ftypes.RefVCS,
URL: url,
},
}
}
return nil
}
func resolveVCSUrl(modulePath string) string {
switch {
case strings.HasPrefix(modulePath, "github.com/"):
return "https://" + VCSUrlMajorVersionSuffixRegex.ReplaceAllString(modulePath, "")
case VCSUrlGoPkgInRegexWithUser.MatchString(modulePath):
return "https://" + VCSUrlGoPkgInRegexWithUser.ReplaceAllString(modulePath, "github.com/$1/$2")
case VCSUrlGoPkgInRegexWithoutUser.MatchString(modulePath):
return "https://" + VCSUrlGoPkgInRegexWithoutUser.ReplaceAllString(modulePath, "github.com/go-$1/$1")
}
return ""
}
// Parse parses a go.mod file
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
pkgs := make(map[string]ftypes.Package)
goModData, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("file read error: %w", err)
}
modFileParsed, err := modfile.Parse("go.mod", goModData, nil)
if err != nil {
return nil, nil, xerrors.Errorf("go.mod parse error: %w", err)
}
skipIndirect := true
if modFileParsed.Go != nil { // Old go.mod file may not include the go version. Go version for these files is less than 1.17
skipIndirect = lessThan117(modFileParsed.Go.Version)
}
// Main module
if m := modFileParsed.Module; m != nil {
ver := strings.TrimPrefix(m.Mod.Version, "v")
pkgs[m.Mod.Path] = ftypes.Package{
ID: packageID(m.Mod.Path, ver),
Name: m.Mod.Path,
Version: ver,
ExternalReferences: p.GetExternalRefs(m.Mod.Path),
Relationship: ftypes.RelationshipRoot,
}
}
// Required modules
for _, require := range modFileParsed.Require {
// Skip indirect dependencies less than Go 1.17
if skipIndirect && require.Indirect {
continue
}
ver := strings.TrimPrefix(require.Mod.Version, "v")
pkgs[require.Mod.Path] = ftypes.Package{
ID: packageID(require.Mod.Path, ver),
Name: require.Mod.Path,
Version: ver,
Relationship: lo.Ternary(require.Indirect, ftypes.RelationshipIndirect, ftypes.RelationshipDirect),
ExternalReferences: p.GetExternalRefs(require.Mod.Path),
}
}
// No need to evaluate the 'replace' directive for indirect dependencies
if p.replace {
for _, rep := range modFileParsed.Replace {
// Check if replaced path is actually in our pkgs.
old, ok := pkgs[rep.Old.Path]
if !ok {
continue
}
// If the replace directive has a version on the left side, make sure it matches the version that was imported.
if rep.Old.Version != "" && old.Version != rep.Old.Version[1:] {
continue
}
// Only support replace directive with version on the right side.
// Directive without version is a local path.
if rep.New.Version == "" {
// Delete old pkg, since it's a local path now.
delete(pkgs, rep.Old.Path)
continue
}
// Delete old pkg, in case the path has changed.
delete(pkgs, rep.Old.Path)
// Add replaced package to package register.
pkgs[rep.New.Path] = ftypes.Package{
ID: packageID(rep.New.Path, rep.New.Version[1:]),
Name: rep.New.Path,
Version: rep.New.Version[1:],
Relationship: old.Relationship,
ExternalReferences: p.GetExternalRefs(rep.New.Path),
}
}
}
return lo.Values(pkgs), nil, nil
}
// Check if the Go version is less than 1.17
func lessThan117(ver string) bool {
ss := strings.Split(ver, ".")
if len(ss) != 2 {
return false
}
major, err := strconv.Atoi(ss[0])
if err != nil {
return false
}
minor, err := strconv.Atoi(ss[1])
if err != nil {
return false
}
return major <= 1 && minor < 17
}
func packageID(name, version string) string {
return dependency.ID(ftypes.GoModule, name, version)
}