Skip to content

Commit

Permalink
feat(rust): Support workspace.members parsing for Cargo.toml analysis (
Browse files Browse the repository at this point in the history
…aquasecurity#5285)

Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
Co-authored-by: knqyf263 <knqyf263@gmail.com>
  • Loading branch information
3 people committed Jan 29, 2024
1 parent 4df9363 commit 5924c02
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 34 deletions.
97 changes: 63 additions & 34 deletions pkg/fanal/analyzer/language/rust/cargo/cargo.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sort"

Expand Down Expand Up @@ -58,18 +59,18 @@ func (a cargoAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysi
return filepath.Base(path) == types.CargoLock
}

err := fsutils.WalkDir(input.FS, ".", required, func(path string, d fs.DirEntry, r io.Reader) error {
err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, d fs.DirEntry, r io.Reader) error {
// Parse Cargo.lock
app, err := a.parseCargoLock(path, r)
app, err := a.parseCargoLock(filePath, r)
if err != nil {
return xerrors.Errorf("parse error: %w", err)
} else if app == nil {
return nil
}

// Parse Cargo.toml alongside Cargo.lock to identify the direct dependencies
if err = a.removeDevDependencies(input.FS, filepath.Dir(path), app); err != nil {
log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", filepath.Join(filepath.Dir(path), types.CargoToml), err)
if err = a.removeDevDependencies(input.FS, path.Dir(filePath), app); err != nil {
log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", path.Join(path.Dir(filePath), types.CargoToml), err)
}
sort.Sort(app.Libraries)
apps = append(apps, *app)
Expand Down Expand Up @@ -98,13 +99,13 @@ func (a cargoAnalyzer) Version() int {
return version
}

func (a cargoAnalyzer) parseCargoLock(path string, r io.Reader) (*types.Application, error) {
return language.Parse(types.Cargo, path, r, a.lockParser)
func (a cargoAnalyzer) parseCargoLock(filePath string, r io.Reader) (*types.Application, error) {
return language.Parse(types.Cargo, filePath, r, a.lockParser)
}

func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.Application) error {
cargoTOMLPath := filepath.Join(dir, types.CargoToml)
directDeps, err := a.parseCargoTOML(fsys, cargoTOMLPath)
cargoTOMLPath := path.Join(dir, types.CargoToml)
directDeps, err := a.parseRootCargoTOML(fsys, cargoTOMLPath)
if errors.Is(err, fs.ErrNotExist) {
log.Logger.Debugf("Cargo: %s not found", cargoTOMLPath)
return nil
Expand Down Expand Up @@ -155,40 +156,38 @@ func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.
type cargoToml struct {
Dependencies Dependencies `toml:"dependencies"`
Target map[string]map[string]Dependencies `toml:"target"`
Workspace map[string]Dependencies `toml:"workspace"`
Workspace cargoTomlWorkspace `toml:"workspace"`
}

type cargoTomlWorkspace struct {
Dependencies Dependencies `toml:"dependencies"`
Members []string `toml:"members"`
}

type Dependencies map[string]interface{}

func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, path string) (map[string]string, error) {
// Parse Cargo.json
f, err := fsys.Open(path)
// parseRootCargoTOML parses top-level Cargo.toml and returns dependencies.
// It also parses workspace members and their dependencies.
func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (map[string]string, error) {
dependencies, members, err := parseCargoTOML(fsys, filePath)
if err != nil {
return nil, xerrors.Errorf("file open error: %w", err)
return nil, xerrors.Errorf("unable to parse %s: %w", filePath, err)
}
defer func() { _ = f.Close() }()

tomlFile := cargoToml{}
deps := make(map[string]string)
_, err = toml.NewDecoder(f).Decode(&tomlFile)
if err != nil {
return nil, xerrors.Errorf("toml decode error: %w", err)
}

// There are cases when toml file doesn't include `Dependencies` field (then map will be nil).
// e.g. when only `workspace.Dependencies` are used
// declare `dependencies` to avoid panic
dependencies := Dependencies{}
maps.Copy(dependencies, tomlFile.Dependencies)

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies
for _, target := range tomlFile.Target {
maps.Copy(dependencies, target["dependencies"])
// According to Cargo workspace RFC, workspaces can't be nested:
// https://github.com/nox/rust-rfcs/blob/master/text/1525-cargo-workspace.md#validating-a-workspace
for _, member := range members {
memberPath := path.Join(path.Dir(filePath), member, types.CargoToml)
memberDeps, _, err := parseCargoTOML(fsys, memberPath)
if err != nil {
log.Logger.Warnf("Unable to parse %q: %s", memberPath, err)
continue
}
// Member dependencies shouldn't overwrite dependencies from root cargo.toml file
maps.Copy(memberDeps, dependencies)
dependencies = memberDeps
}

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
maps.Copy(dependencies, tomlFile.Workspace["dependencies"])

deps := make(map[string]string)
for name, value := range dependencies {
switch ver := value.(type) {
case string:
Expand Down Expand Up @@ -250,3 +249,33 @@ func (a cargoAnalyzer) matchVersion(currentVersion, constraint string) (bool, er

return c.Check(ver), nil
}

func parseCargoTOML(fsys fs.FS, filePath string) (Dependencies, []string, error) {
// Parse Cargo.toml
f, err := fsys.Open(filePath)
if err != nil {
return nil, nil, xerrors.Errorf("file open error: %w", err)
}
defer func() { _ = f.Close() }()

var tomlFile cargoToml
// There are cases when toml file doesn't include `Dependencies` field (then map will be nil).
// e.g. when only `workspace.Dependencies` are used
// declare `dependencies` to avoid panic
dependencies := Dependencies{}
if _, err = toml.NewDecoder(f).Decode(&tomlFile); err != nil {
return nil, nil, xerrors.Errorf("toml decode error: %w", err)
}

maps.Copy(dependencies, tomlFile.Dependencies)

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies
for _, target := range tomlFile.Target {
maps.Copy(dependencies, target["dependencies"])
}

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
maps.Copy(dependencies, tomlFile.Workspace.Dependencies)
// https://doc.rust-lang.org/cargo/reference/workspaces.html#the-members-and-exclude-fields
return dependencies, tomlFile.Workspace.Members, nil
}
126 changes: 126 additions & 0 deletions pkg/fanal/analyzer/language/rust/cargo/cargo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,132 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) {
dir: "testdata/sad",
want: &analyzer.AnalysisResult{},
},
{
name: "workspace members",
dir: "testdata/toml-workspace-members",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.Cargo,
FilePath: "Cargo.lock",
Libraries: types.Packages{
{
ID: "aho-corasick@1.1.2",
Name: "aho-corasick",
Version: "1.1.2",
Indirect: true,
Locations: []types.Location{
{
StartLine: 5,
EndLine: 12,
},
},
DependsOn: []string{"memchr@2.6.4"},
},
{
ID: "gdb-command@0.7.6",
Name: "gdb-command",
Version: "0.7.6",
Indirect: false,
Locations: []types.Location{
{
StartLine: 14,
EndLine: 22,
},
},
DependsOn: []string{
"regex@1.10.2",
"wait-timeout@0.2.0",
},
},
{
ID: "libc@0.2.150",
Name: "libc",
Version: "0.2.150",
Indirect: true,
Locations: []types.Location{
{
StartLine: 24,
EndLine: 28,
},
},
},
{
ID: "memchr@2.6.4",
Name: "memchr",
Version: "2.6.4",
Indirect: true,
Locations: []types.Location{
{
StartLine: 44,
EndLine: 48,
},
},
},
{
ID: "regex@1.10.2",
Name: "regex",
Version: "1.10.2",
Locations: []types.Location{
{
StartLine: 50,
EndLine: 60,
},
},
DependsOn: []string{
"aho-corasick@1.1.2",
"memchr@2.6.4",
"regex-automata@0.4.3",
"regex-syntax@0.8.2",
},
},
{
ID: "regex-automata@0.4.3",
Name: "regex-automata",
Version: "0.4.3",
Indirect: true,
Locations: []types.Location{
{
StartLine: 62,
EndLine: 71,
},
},
DependsOn: []string{
"aho-corasick@1.1.2",
"memchr@2.6.4",
"regex-syntax@0.8.2",
},
},
{
ID: "regex-syntax@0.8.2",
Name: "regex-syntax",
Version: "0.8.2",
Indirect: true,
Locations: []types.Location{
{
StartLine: 73,
EndLine: 77,
},
},
},
{
ID: "wait-timeout@0.2.0",
Name: "wait-timeout",
Version: "0.2.0",
Indirect: true,
Locations: []types.Location{
{
StartLine: 79,
EndLine: 86,
},
},
DependsOn: []string{"libc@0.2.150"},
},
},
},
},
},
},
}

for _, tt := range tests {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[workspace.package]
name = "toml-workspace-members"
version = "0.1.0"

[workspace]
resolver = "2"
members = ["member", "member2"]

[workspace.dependencies]
regex = "1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "member"
version = "0.1.0"
edition = "2021"

[dependencies]
gdb-command = "0.7"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
edition = "2021"
name = "member2"
version = "0.1.0"

[dependencies]
regex = { workspace = true}

0 comments on commit 5924c02

Please sign in to comment.