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 }