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

     1  package composer
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  
    14  	"golang.org/x/exp/slices"
    15  	"golang.org/x/xerrors"
    16  
    17  	"github.com/aquasecurity/go-dep-parser/pkg/php/composer"
    18  	godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
    19  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    20  	"github.com/devseccon/trivy/pkg/fanal/analyzer/language"
    21  	"github.com/devseccon/trivy/pkg/fanal/types"
    22  	"github.com/devseccon/trivy/pkg/log"
    23  	"github.com/devseccon/trivy/pkg/utils/fsutils"
    24  )
    25  
    26  func init() {
    27  	analyzer.RegisterPostAnalyzer(analyzer.TypeComposer, newComposerAnalyzer)
    28  }
    29  
    30  const version = 1
    31  
    32  var requiredFiles = []string{
    33  	types.ComposerLock,
    34  	types.ComposerJson,
    35  }
    36  
    37  type composerAnalyzer struct {
    38  	lockParser godeptypes.Parser
    39  }
    40  
    41  func newComposerAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
    42  	return &composerAnalyzer{
    43  		lockParser: composer.NewParser(),
    44  	}, nil
    45  }
    46  
    47  func (a composerAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
    48  	var apps []types.Application
    49  
    50  	required := func(path string, d fs.DirEntry) bool {
    51  		return filepath.Base(path) == types.ComposerLock
    52  	}
    53  
    54  	err := fsutils.WalkDir(input.FS, ".", required, func(path string, d fs.DirEntry, r io.Reader) error {
    55  		// Parse composer.lock
    56  		app, err := a.parseComposerLock(path, r)
    57  		if err != nil {
    58  			return xerrors.Errorf("parse error: %w", err)
    59  		} else if app == nil {
    60  			return nil
    61  		}
    62  
    63  		// Parse composer.json alongside composer.lock to identify the direct dependencies
    64  		if err = a.mergeComposerJson(input.FS, filepath.Dir(path), app); err != nil {
    65  			log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", filepath.Join(filepath.Dir(path), types.ComposerJson), err)
    66  		}
    67  		sort.Sort(app.Libraries)
    68  		apps = append(apps, *app)
    69  
    70  		return nil
    71  	})
    72  	if err != nil {
    73  		return nil, xerrors.Errorf("composer walk error: %w", err)
    74  	}
    75  
    76  	return &analyzer.AnalysisResult{
    77  		Applications: apps,
    78  	}, nil
    79  }
    80  
    81  func (a composerAnalyzer) Required(filePath string, _ os.FileInfo) bool {
    82  	fileName := filepath.Base(filePath)
    83  	if !slices.Contains(requiredFiles, fileName) {
    84  		return false
    85  	}
    86  
    87  	// Skip `composer.lock` inside `vendor` folder
    88  	if slices.Contains(strings.Split(filePath, "/"), "vendor") {
    89  		return false
    90  	}
    91  	return true
    92  }
    93  
    94  func (a composerAnalyzer) Type() analyzer.Type {
    95  	return analyzer.TypeComposer
    96  }
    97  
    98  func (a composerAnalyzer) Version() int {
    99  	return version
   100  }
   101  
   102  func (a composerAnalyzer) parseComposerLock(path string, r io.Reader) (*types.Application, error) {
   103  	return language.Parse(types.Composer, path, r, a.lockParser)
   104  }
   105  
   106  func (a composerAnalyzer) mergeComposerJson(fsys fs.FS, dir string, app *types.Application) error {
   107  	// Parse composer.json to identify the direct dependencies
   108  	path := filepath.Join(dir, types.ComposerJson)
   109  	p, err := a.parseComposerJson(fsys, path)
   110  	if errors.Is(err, fs.ErrNotExist) {
   111  		// Assume all the packages are direct dependencies as it cannot identify them from composer.lock
   112  		log.Logger.Debugf("Unable to determine the direct dependencies: %s not found", path)
   113  		return nil
   114  	} else if err != nil {
   115  		return xerrors.Errorf("unable to parse %s: %w", path, err)
   116  	}
   117  
   118  	for i, lib := range app.Libraries {
   119  		// Identify the direct/transitive dependencies
   120  		if _, ok := p[lib.Name]; !ok {
   121  			app.Libraries[i].Indirect = true
   122  		}
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  type composerJson struct {
   129  	Require map[string]string `json:"require"`
   130  }
   131  
   132  func (a composerAnalyzer) parseComposerJson(fsys fs.FS, path string) (map[string]string, error) {
   133  	// Parse composer.json
   134  	f, err := fsys.Open(path)
   135  	if err != nil {
   136  		return nil, xerrors.Errorf("file open error: %w", err)
   137  	}
   138  	defer func() { _ = f.Close() }()
   139  
   140  	jsonFile := composerJson{}
   141  	err = json.NewDecoder(f).Decode(&jsonFile)
   142  	if err != nil {
   143  		return nil, xerrors.Errorf("json decode error: %w", err)
   144  	}
   145  	return jsonFile.Require, nil
   146  }