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 }