github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/analyzer/language/python/poetry/poetry.go (about) 1 package poetry 2 3 import ( 4 "context" 5 "errors" 6 "io" 7 "io/fs" 8 "os" 9 "path/filepath" 10 11 "golang.org/x/xerrors" 12 13 "github.com/aquasecurity/go-dep-parser/pkg/python/poetry" 14 "github.com/aquasecurity/go-dep-parser/pkg/python/pyproject" 15 godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types" 16 "github.com/devseccon/trivy/pkg/fanal/analyzer" 17 "github.com/devseccon/trivy/pkg/fanal/analyzer/language" 18 "github.com/devseccon/trivy/pkg/fanal/types" 19 "github.com/devseccon/trivy/pkg/log" 20 "github.com/devseccon/trivy/pkg/utils/fsutils" 21 ) 22 23 func init() { 24 analyzer.RegisterPostAnalyzer(analyzer.TypePoetry, newPoetryAnalyzer) 25 } 26 27 const version = 1 28 29 type poetryAnalyzer struct { 30 pyprojectParser *pyproject.Parser 31 lockParser godeptypes.Parser 32 } 33 34 func newPoetryAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { 35 return &poetryAnalyzer{ 36 pyprojectParser: pyproject.NewParser(), 37 lockParser: poetry.NewParser(), 38 }, nil 39 } 40 41 func (a poetryAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { 42 var apps []types.Application 43 44 required := func(path string, d fs.DirEntry) bool { 45 return filepath.Base(path) == types.PoetryLock 46 } 47 48 err := fsutils.WalkDir(input.FS, ".", required, func(path string, d fs.DirEntry, r io.Reader) error { 49 // Parse poetry.lock 50 app, err := a.parsePoetryLock(path, r) 51 if err != nil { 52 return xerrors.Errorf("parse error: %w", err) 53 } else if app == nil { 54 return nil 55 } 56 57 // Parse pyproject.toml alongside poetry.lock to identify the direct dependencies 58 if err = a.mergePyProject(input.FS, filepath.Dir(path), app); err != nil { 59 log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", filepath.Join(filepath.Dir(path), types.PyProject), err) 60 } 61 apps = append(apps, *app) 62 63 return nil 64 }) 65 if err != nil { 66 return nil, xerrors.Errorf("poetry walk error: %w", err) 67 } 68 69 return &analyzer.AnalysisResult{ 70 Applications: apps, 71 }, nil 72 } 73 74 func (a poetryAnalyzer) Required(filePath string, _ os.FileInfo) bool { 75 fileName := filepath.Base(filePath) 76 return fileName == types.PoetryLock || fileName == types.PyProject 77 } 78 79 func (a poetryAnalyzer) Type() analyzer.Type { 80 return analyzer.TypePoetry 81 } 82 83 func (a poetryAnalyzer) Version() int { 84 return version 85 } 86 87 func (a poetryAnalyzer) parsePoetryLock(path string, r io.Reader) (*types.Application, error) { 88 return language.Parse(types.Poetry, path, r, a.lockParser) 89 } 90 91 func (a poetryAnalyzer) mergePyProject(fsys fs.FS, dir string, app *types.Application) error { 92 // Parse pyproject.toml to identify the direct dependencies 93 path := filepath.Join(dir, types.PyProject) 94 p, err := a.parsePyProject(fsys, path) 95 if errors.Is(err, fs.ErrNotExist) { 96 // Assume all the packages are direct dependencies as it cannot identify them from poetry.lock 97 log.Logger.Debugf("Poetry: %s not found", path) 98 return nil 99 } else if err != nil { 100 return xerrors.Errorf("unable to parse %s: %w", path, err) 101 } 102 103 for i, lib := range app.Libraries { 104 // Identify the direct/transitive dependencies 105 if _, ok := p[lib.Name]; !ok { 106 app.Libraries[i].Indirect = true 107 } 108 } 109 110 return nil 111 } 112 113 func (a poetryAnalyzer) parsePyProject(fsys fs.FS, path string) (map[string]interface{}, error) { 114 // Parse pyproject.toml 115 f, err := fsys.Open(path) 116 if err != nil { 117 return nil, xerrors.Errorf("file open error: %w", err) 118 } 119 defer f.Close() 120 121 parsed, err := a.pyprojectParser.Parse(f) 122 if err != nil { 123 return nil, err 124 } 125 return parsed, nil 126 }