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

     1  package cargo
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  
    13  	"github.com/BurntSushi/toml"
    14  	"github.com/samber/lo"
    15  	"golang.org/x/exp/maps"
    16  	"golang.org/x/exp/slices"
    17  	"golang.org/x/xerrors"
    18  
    19  	"github.com/aquasecurity/go-dep-parser/pkg/rust/cargo"
    20  	godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
    21  	"github.com/aquasecurity/go-version/pkg/semver"
    22  	goversion "github.com/aquasecurity/go-version/pkg/version"
    23  	"github.com/devseccon/trivy/pkg/detector/library/compare"
    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/log"
    28  	"github.com/devseccon/trivy/pkg/utils/fsutils"
    29  )
    30  
    31  func init() {
    32  	analyzer.RegisterPostAnalyzer(analyzer.TypeCargo, newCargoAnalyzer)
    33  }
    34  
    35  const version = 1
    36  
    37  var requiredFiles = []string{
    38  	types.CargoLock,
    39  	types.CargoToml,
    40  }
    41  
    42  type cargoAnalyzer struct {
    43  	lockParser godeptypes.Parser
    44  	comparer   compare.GenericComparer
    45  }
    46  
    47  func newCargoAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
    48  	return &cargoAnalyzer{
    49  		lockParser: cargo.NewParser(),
    50  		comparer:   compare.GenericComparer{},
    51  	}, nil
    52  }
    53  
    54  func (a cargoAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
    55  	var apps []types.Application
    56  
    57  	required := func(path string, d fs.DirEntry) bool {
    58  		return filepath.Base(path) == types.CargoLock
    59  	}
    60  
    61  	err := fsutils.WalkDir(input.FS, ".", required, func(path string, d fs.DirEntry, r io.Reader) error {
    62  		// Parse Cargo.lock
    63  		app, err := a.parseCargoLock(path, r)
    64  		if err != nil {
    65  			return xerrors.Errorf("parse error: %w", err)
    66  		} else if app == nil {
    67  			return nil
    68  		}
    69  
    70  		// Parse Cargo.toml alongside Cargo.lock to identify the direct dependencies
    71  		if err = a.removeDevDependencies(input.FS, filepath.Dir(path), app); err != nil {
    72  			log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", filepath.Join(filepath.Dir(path), types.CargoToml), err)
    73  		}
    74  		sort.Sort(app.Libraries)
    75  		apps = append(apps, *app)
    76  
    77  		return nil
    78  	})
    79  	if err != nil {
    80  		return nil, xerrors.Errorf("cargo walk error: %w", err)
    81  	}
    82  
    83  	return &analyzer.AnalysisResult{
    84  		Applications: apps,
    85  	}, nil
    86  }
    87  
    88  func (a cargoAnalyzer) Required(filePath string, _ os.FileInfo) bool {
    89  	fileName := filepath.Base(filePath)
    90  	return slices.Contains(requiredFiles, fileName)
    91  }
    92  
    93  func (a cargoAnalyzer) Type() analyzer.Type {
    94  	return analyzer.TypeCargo
    95  }
    96  
    97  func (a cargoAnalyzer) Version() int {
    98  	return version
    99  }
   100  
   101  func (a cargoAnalyzer) parseCargoLock(path string, r io.Reader) (*types.Application, error) {
   102  	return language.Parse(types.Cargo, path, r, a.lockParser)
   103  }
   104  
   105  func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.Application) error {
   106  	cargoTOMLPath := filepath.Join(dir, types.CargoToml)
   107  	directDeps, err := a.parseCargoTOML(fsys, cargoTOMLPath)
   108  	if errors.Is(err, fs.ErrNotExist) {
   109  		log.Logger.Debugf("Cargo: %s not found", cargoTOMLPath)
   110  		return nil
   111  	} else if err != nil {
   112  		return xerrors.Errorf("unable to parse %s: %w", cargoTOMLPath, err)
   113  	}
   114  
   115  	// Cargo.toml file can contain same libraries with different versions.
   116  	// Save versions separately for version comparison by comparator
   117  	pkgIDs := lo.SliceToMap(app.Libraries, func(pkg types.Package) (string, types.Package) {
   118  		return pkg.ID, pkg
   119  	})
   120  
   121  	// Identify direct dependencies
   122  	pkgs := make(map[string]types.Package)
   123  	for name, constraint := range directDeps {
   124  		for _, pkg := range app.Libraries {
   125  			if pkg.Name != name {
   126  				continue
   127  			}
   128  
   129  			if match, err := a.matchVersion(pkg.Version, constraint); err != nil {
   130  				log.Logger.Warnf("Unable to match Cargo version: package: %s, error: %s", pkg.ID, err)
   131  				continue
   132  			} else if match {
   133  				// Mark as a direct dependency
   134  				pkg.Indirect = false
   135  				pkgs[pkg.ID] = pkg
   136  				break
   137  			}
   138  		}
   139  	}
   140  
   141  	// Walk indirect dependencies
   142  	// Since it starts from direct dependencies, devDependencies will not appear in this walk.
   143  	for _, pkg := range pkgs {
   144  		a.walkIndirectDependencies(pkg, pkgIDs, pkgs)
   145  	}
   146  
   147  	pkgSlice := maps.Values(pkgs)
   148  	sort.Sort(types.Packages(pkgSlice))
   149  
   150  	// Save only prod libraries
   151  	app.Libraries = pkgSlice
   152  	return nil
   153  }
   154  
   155  type cargoToml struct {
   156  	Dependencies Dependencies                       `toml:"dependencies"`
   157  	Target       map[string]map[string]Dependencies `toml:"target"`
   158  	Workspace    map[string]Dependencies            `toml:"workspace"`
   159  }
   160  
   161  type Dependencies map[string]interface{}
   162  
   163  func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, path string) (map[string]string, error) {
   164  	// Parse Cargo.json
   165  	f, err := fsys.Open(path)
   166  	if err != nil {
   167  		return nil, xerrors.Errorf("file open error: %w", err)
   168  	}
   169  	defer func() { _ = f.Close() }()
   170  
   171  	tomlFile := cargoToml{}
   172  	deps := make(map[string]string)
   173  	_, err = toml.NewDecoder(f).Decode(&tomlFile)
   174  	if err != nil {
   175  		return nil, xerrors.Errorf("toml decode error: %w", err)
   176  	}
   177  
   178  	// There are cases when toml file doesn't include `Dependencies` field (then map will be nil).
   179  	// e.g. when only `workspace.Dependencies` are used
   180  	// declare `dependencies` to avoid panic
   181  	dependencies := Dependencies{}
   182  	maps.Copy(dependencies, tomlFile.Dependencies)
   183  
   184  	// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies
   185  	for _, target := range tomlFile.Target {
   186  		maps.Copy(dependencies, target["dependencies"])
   187  	}
   188  
   189  	// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
   190  	maps.Copy(dependencies, tomlFile.Workspace["dependencies"])
   191  
   192  	for name, value := range dependencies {
   193  		switch ver := value.(type) {
   194  		case string:
   195  			// e.g. regex = "1.5"
   196  			deps[name] = ver
   197  		case map[string]interface{}:
   198  			// e.g. serde = { version = "1.0", features = ["derive"] }
   199  			for k, v := range ver {
   200  				if k == "version" {
   201  					if vv, ok := v.(string); ok {
   202  						deps[name] = vv
   203  					}
   204  					break
   205  				}
   206  			}
   207  		}
   208  	}
   209  
   210  	return deps, nil
   211  }
   212  
   213  func (a cargoAnalyzer) walkIndirectDependencies(pkg types.Package, pkgIDs, deps map[string]types.Package) {
   214  	for _, pkgID := range pkg.DependsOn {
   215  		if _, ok := deps[pkgID]; ok {
   216  			continue
   217  		}
   218  
   219  		dep, ok := pkgIDs[pkgID]
   220  		if !ok {
   221  			continue
   222  		}
   223  
   224  		dep.Indirect = true
   225  		deps[dep.ID] = dep
   226  		a.walkIndirectDependencies(dep, pkgIDs, deps)
   227  	}
   228  }
   229  
   230  // cf. https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
   231  func (a cargoAnalyzer) matchVersion(currentVersion, constraint string) (bool, error) {
   232  	// `` == `^` - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#caret-requirements
   233  	// Add `^` for correct version comparison
   234  	//   - 1.2.3 -> ^1.2.3
   235  	//   - 1.2.* -> 1.2.*
   236  	//   - ^1.2  -> ^1.2
   237  	if _, err := goversion.Parse(constraint); err == nil {
   238  		constraint = fmt.Sprintf("^%s", constraint)
   239  	}
   240  
   241  	ver, err := semver.Parse(currentVersion)
   242  	if err != nil {
   243  		return false, xerrors.Errorf("version error (%s): %s", currentVersion, err)
   244  	}
   245  
   246  	c, err := semver.NewConstraints(constraint)
   247  	if err != nil {
   248  		return false, xerrors.Errorf("constraint error (%s): %s", currentVersion, err)
   249  	}
   250  
   251  	return c.Check(ver), nil
   252  }