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  }