github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/analyzer/language/golang/mod/mod.go (about)

     1  package mod
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"go/build"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"unicode"
    14  
    15  	"github.com/samber/lo"
    16  	"golang.org/x/exp/maps"
    17  	"golang.org/x/exp/slices"
    18  	"golang.org/x/xerrors"
    19  
    20  	"github.com/aquasecurity/go-dep-parser/pkg/golang/mod"
    21  	"github.com/aquasecurity/go-dep-parser/pkg/golang/sum"
    22  	dio "github.com/aquasecurity/go-dep-parser/pkg/io"
    23  	godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
    24  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    25  	"github.com/devseccon/trivy/pkg/fanal/analyzer/language"
    26  	"github.com/devseccon/trivy/pkg/fanal/types"
    27  	"github.com/devseccon/trivy/pkg/licensing"
    28  	"github.com/devseccon/trivy/pkg/log"
    29  	"github.com/devseccon/trivy/pkg/utils/fsutils"
    30  )
    31  
    32  func init() {
    33  	analyzer.RegisterPostAnalyzer(analyzer.TypeGoMod, newGoModAnalyzer)
    34  }
    35  
    36  const version = 2
    37  
    38  var (
    39  	requiredFiles = []string{
    40  		types.GoMod,
    41  		types.GoSum,
    42  	}
    43  	licenseRegexp = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING|README|NOTICE).*$`)
    44  )
    45  
    46  type gomodAnalyzer struct {
    47  	// root go.mod/go.sum
    48  	modParser godeptypes.Parser
    49  	sumParser godeptypes.Parser
    50  
    51  	// go.mod/go.sum in dependencies
    52  	leafModParser godeptypes.Parser
    53  
    54  	licenseClassifierConfidenceLevel float64
    55  }
    56  
    57  func newGoModAnalyzer(opt analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
    58  	return &gomodAnalyzer{
    59  		modParser:                        mod.NewParser(true), // Only the root module should replace
    60  		sumParser:                        sum.NewParser(),
    61  		leafModParser:                    mod.NewParser(false),
    62  		licenseClassifierConfidenceLevel: opt.LicenseScannerOption.ClassifierConfidenceLevel,
    63  	}, nil
    64  }
    65  
    66  func (a *gomodAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
    67  	var apps []types.Application
    68  
    69  	required := func(path string, d fs.DirEntry) bool {
    70  		return filepath.Base(path) == types.GoMod
    71  	}
    72  
    73  	err := fsutils.WalkDir(input.FS, ".", required, func(path string, d fs.DirEntry, _ io.Reader) error {
    74  		// Parse go.mod
    75  		gomod, err := parse(input.FS, path, a.modParser)
    76  		if err != nil {
    77  			return xerrors.Errorf("parse error: %w", err)
    78  		} else if gomod == nil {
    79  			return nil
    80  		}
    81  
    82  		if lessThanGo117(gomod) {
    83  			// e.g. /app/go.mod => /app/go.sum
    84  			sumPath := filepath.Join(filepath.Dir(path), types.GoSum)
    85  			gosum, err := parse(input.FS, sumPath, a.sumParser)
    86  			if err != nil && !errors.Is(err, fs.ErrNotExist) {
    87  				return xerrors.Errorf("parse error: %w", err)
    88  			}
    89  			mergeGoSum(gomod, gosum)
    90  		}
    91  
    92  		apps = append(apps, *gomod)
    93  		return nil
    94  	})
    95  	if err != nil {
    96  		return nil, xerrors.Errorf("walk error: %w", err)
    97  	}
    98  
    99  	if err = a.fillAdditionalData(apps); err != nil {
   100  		log.Logger.Warnf("Unable to collect additional info: %s", err)
   101  	}
   102  
   103  	return &analyzer.AnalysisResult{
   104  		Applications: apps,
   105  	}, nil
   106  }
   107  
   108  func (a *gomodAnalyzer) Required(filePath string, _ os.FileInfo) bool {
   109  	fileName := filepath.Base(filePath)
   110  	return slices.Contains(requiredFiles, fileName)
   111  }
   112  
   113  func (a *gomodAnalyzer) Type() analyzer.Type {
   114  	return analyzer.TypeGoMod
   115  }
   116  
   117  func (a *gomodAnalyzer) Version() int {
   118  	return version
   119  }
   120  
   121  // fillAdditionalData collects licenses and dependency relationships, then update applications.
   122  func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application) error {
   123  	gopath := os.Getenv("GOPATH")
   124  	if gopath == "" {
   125  		gopath = build.Default.GOPATH
   126  	}
   127  
   128  	// $GOPATH/pkg/mod
   129  	modPath := filepath.Join(gopath, "pkg", "mod")
   130  	if !fsutils.DirExists(modPath) {
   131  		log.Logger.Debugf("GOPATH (%s) not found. Need 'go mod download' to fill licenses and dependency relationships", modPath)
   132  		return nil
   133  	}
   134  
   135  	licenses := make(map[string][]string)
   136  	for i, app := range apps {
   137  		// Actually used dependencies
   138  		usedLibs := lo.SliceToMap(app.Libraries, func(pkg types.Package) (string, types.Package) {
   139  			return pkg.Name, pkg
   140  		})
   141  		for j, lib := range app.Libraries {
   142  			if l, ok := licenses[lib.ID]; ok {
   143  				// Fill licenses
   144  				apps[i].Libraries[j].Licenses = l
   145  				continue
   146  			}
   147  
   148  			// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v1.0.0
   149  			modDir := filepath.Join(modPath, fmt.Sprintf("%s@v%s", normalizeModName(lib.Name), lib.Version))
   150  
   151  			// Collect licenses
   152  			if licenseNames, err := findLicense(modDir, a.licenseClassifierConfidenceLevel); err != nil {
   153  				return xerrors.Errorf("license error: %w", err)
   154  			} else {
   155  				// Cache the detected licenses
   156  				licenses[lib.ID] = licenseNames
   157  
   158  				// Fill licenses
   159  				apps[i].Libraries[j].Licenses = licenseNames
   160  			}
   161  
   162  			// Collect dependencies of the direct dependency
   163  			if dep, err := a.collectDeps(modDir, lib.ID); err != nil {
   164  				return xerrors.Errorf("dependency graph error: %w", err)
   165  			} else if dep.ID == "" {
   166  				// go.mod not found
   167  				continue
   168  			} else {
   169  				// Filter out unused dependencies and convert module names to module IDs
   170  				apps[i].Libraries[j].DependsOn = lo.FilterMap(dep.DependsOn, func(modName string, _ int) (string, bool) {
   171  					if m, ok := usedLibs[modName]; !ok {
   172  						return "", false
   173  					} else {
   174  						return m.ID, true
   175  					}
   176  				})
   177  			}
   178  		}
   179  	}
   180  	return nil
   181  }
   182  
   183  func (a *gomodAnalyzer) collectDeps(modDir, pkgID string) (godeptypes.Dependency, error) {
   184  	// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/go.mod
   185  	modPath := filepath.Join(modDir, "go.mod")
   186  	f, err := os.Open(modPath)
   187  	if errors.Is(err, fs.ErrNotExist) {
   188  		log.Logger.Debugf("Unable to identify dependencies of %s as it doesn't support Go modules", pkgID)
   189  		return godeptypes.Dependency{}, nil
   190  	} else if err != nil {
   191  		return godeptypes.Dependency{}, xerrors.Errorf("file open error: %w", err)
   192  	}
   193  	defer f.Close()
   194  
   195  	// Parse go.mod under $GOPATH/pkg/mod
   196  	libs, _, err := a.leafModParser.Parse(f)
   197  	if err != nil {
   198  		return godeptypes.Dependency{}, xerrors.Errorf("%s parse error: %w", modPath, err)
   199  	}
   200  
   201  	// Filter out indirect dependencies
   202  	dependsOn := lo.FilterMap(libs, func(lib godeptypes.Library, index int) (string, bool) {
   203  		return lib.Name, !lib.Indirect
   204  	})
   205  
   206  	return godeptypes.Dependency{
   207  		ID:        pkgID,
   208  		DependsOn: dependsOn,
   209  	}, nil
   210  }
   211  
   212  func parse(fsys fs.FS, path string, parser godeptypes.Parser) (*types.Application, error) {
   213  	f, err := fsys.Open(path)
   214  	if err != nil {
   215  		return nil, xerrors.Errorf("file open error: %w", err)
   216  	}
   217  	defer f.Close()
   218  
   219  	file, ok := f.(dio.ReadSeekCloserAt)
   220  	if !ok {
   221  		return nil, xerrors.Errorf("type assertion error: %w", err)
   222  	}
   223  
   224  	// Parse go.mod or go.sum
   225  	return language.Parse(types.GoModule, path, file, parser)
   226  }
   227  
   228  func lessThanGo117(gomod *types.Application) bool {
   229  	for _, lib := range gomod.Libraries {
   230  		// The indirect field is populated only in Go 1.17+
   231  		if lib.Indirect {
   232  			return false
   233  		}
   234  	}
   235  	return true
   236  }
   237  
   238  func mergeGoSum(gomod, gosum *types.Application) {
   239  	if gomod == nil || gosum == nil {
   240  		return
   241  	}
   242  	uniq := make(map[string]types.Package)
   243  	for _, lib := range gomod.Libraries {
   244  		// It will be used for merging go.sum.
   245  		uniq[lib.Name] = lib
   246  	}
   247  
   248  	// For Go 1.16 or less, we need to merge go.sum into go.mod.
   249  	for _, lib := range gosum.Libraries {
   250  		// Skip dependencies in go.mod so that go.mod should be preferred.
   251  		if _, ok := uniq[lib.Name]; ok {
   252  			continue
   253  		}
   254  
   255  		// This dependency doesn't exist in go.mod, so it must be an indirect dependency.
   256  		lib.Indirect = true
   257  		uniq[lib.Name] = lib
   258  	}
   259  
   260  	gomod.Libraries = maps.Values(uniq)
   261  }
   262  
   263  func findLicense(dir string, classifierConfidenceLevel float64) ([]string, error) {
   264  	var license *types.LicenseFile
   265  	err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
   266  		if err != nil {
   267  			return err
   268  		} else if !d.Type().IsRegular() {
   269  			return nil
   270  		}
   271  		if !licenseRegexp.MatchString(filepath.Base(path)) {
   272  			return nil
   273  		}
   274  		// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/LICENSE
   275  		f, err := os.Open(path)
   276  		if err != nil {
   277  			return xerrors.Errorf("file (%s) open error: %w", path, err)
   278  		}
   279  		defer f.Close()
   280  
   281  		l, err := licensing.Classify(path, f, classifierConfidenceLevel)
   282  		if err != nil {
   283  			return xerrors.Errorf("license classify error: %w", err)
   284  		}
   285  		// License found
   286  		if l != nil && len(l.Findings) > 0 {
   287  			license = l
   288  			return io.EOF
   289  		}
   290  		return nil
   291  	})
   292  
   293  	switch {
   294  	// The module path may not exist
   295  	case errors.Is(err, os.ErrNotExist):
   296  		return nil, nil
   297  	case err != nil && !errors.Is(err, io.EOF):
   298  		return nil, fmt.Errorf("finding a known open source license: %w", err)
   299  	case license == nil || len(license.Findings) == 0:
   300  		return nil, nil
   301  	}
   302  
   303  	return license.Findings.Names(), nil
   304  }
   305  
   306  // normalizeModName escapes upper characters
   307  // e.g. 'github.com/BurntSushi/toml' => 'github.com/!burnt!sushi'
   308  func normalizeModName(name string) string {
   309  	var newName []rune
   310  	for _, c := range name {
   311  		if unicode.IsUpper(c) {
   312  			// 'A' => '!a'
   313  			newName = append(newName, '!', unicode.ToLower(c))
   314  		} else {
   315  			newName = append(newName, c)
   316  		}
   317  	}
   318  	return string(newName)
   319  }