Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(go): support dependency graph and show only direct dependencies in the tree #3691

Merged
merged 11 commits into from
Feb 28, 2023
2 changes: 2 additions & 0 deletions docs/docs/vulnerability/examples/report.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ The following packages/languages are currently supported:
- Bundler: Gemfile.lock
- Rust
- Binaries built with [cargo-auditable][cargo-auditable]
- Go
- Modules: go.mod

This tree is the reverse of the npm list command.
However, if you want to resolve a vulnerability in a particular indirect dependency, the reversed tree is useful to know where that dependency comes from and identify which package you actually need to update.
Expand Down
14 changes: 9 additions & 5 deletions docs/docs/vulnerability/languages/golang.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
Trivy supports two types of Go scanning, Go Modules and binaries built by Go.
The following table provides an outline of the features Trivy offers.

| Artifact | Offline[^1] | Dev dependencies | License |
|----------|:-----------:|:-----------------|:-------:|
| Modules | ✓ | Include | ✓[^2] |
| Binaries | ✓ | Exclude | - |
| Artifact | Offline[^1] | Dev dependencies | License | Dependency graph |
|----------|:-----------:|:-----------------|:-------:|:----------------:|
| Modules | ✓ | Include | ✓[^2] | ✓[^2] |
| Binaries | ✓ | Exclude | - | - |

!!! note
Trivy scans only dependencies of the Go project.
Expand Down Expand Up @@ -49,6 +49,10 @@ If you want to have better detection, please consider updating the Go version in
$ go mod tidy -go=1.18
```

To identify licenses and dependency relationships, you need to download modules to local cache beforehand,
such as `go mod download`, `go mod tidy`, etc.
Trivy traverses `$GOPATH/pkg/mod` and collect those extra information.

### Go binaries
Trivy scans binaries built by Go.
If there is a Go binary in your container image, Trivy automatically finds and scans it.
Expand All @@ -60,4 +64,4 @@ $ trivy fs ./your_binary
```

[^1]: It doesn't require the Internet access.
[^2]: Need to download modules to local cache beforehand, like `go mod download`
[^2]: Need to download modules to local cache beforehand
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/alicebob/miniredis/v2 v2.23.0
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986
github.com/aquasecurity/defsec v0.82.11-0.20230227200028-1372c6329e1f
github.com/aquasecurity/go-dep-parser v0.0.0-20230227085514-f6e7eca87043
github.com/aquasecurity/go-dep-parser v0.0.0-20230228091112-63a15cdc6bc3
github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce
github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,8 @@ github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8=
github.com/aquasecurity/defsec v0.82.11-0.20230227200028-1372c6329e1f h1:GMsUbqXjrGzNOyxFGJt/WtJrBFXzQWtJJ2h+uoAUguQ=
github.com/aquasecurity/defsec v0.82.11-0.20230227200028-1372c6329e1f/go.mod h1:AJswzQrwesjdpF03Ev7lcPdr5REBJLAmDqjvOitvr94=
github.com/aquasecurity/go-dep-parser v0.0.0-20230227085514-f6e7eca87043 h1:3YbIYXC9/HLODABe2P4v3nPJjK9MEFOtOmnSbiCIXdo=
github.com/aquasecurity/go-dep-parser v0.0.0-20230227085514-f6e7eca87043/go.mod h1:xx5OX/gVENa5dY60k9EliVvTbUf/EmRw1tJKzdskKGw=
github.com/aquasecurity/go-dep-parser v0.0.0-20230228091112-63a15cdc6bc3 h1:vOnS6iiH+b056rNsJjg6Km/a6N9oX52Cw83lTI3qlFI=
github.com/aquasecurity/go-dep-parser v0.0.0-20230228091112-63a15cdc6bc3/go.mod h1:xx5OX/gVENa5dY60k9EliVvTbUf/EmRw1tJKzdskKGw=
github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM=
github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s=
github.com/aquasecurity/go-mock-aws v0.0.0-20220726154943-99847deb62b0 h1:tihCUjLWkF0b1SAjAKcFltUs3SpsqGrLtI+Frye0D10=
Expand Down
4 changes: 4 additions & 0 deletions integration/testdata/gomod-skip.json.golden
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"Vulnerabilities": [
{
"VulnerabilityID": "GMS-2022-20",
"PkgID": "github.com/docker/[email protected]+incompatible",
"PkgName": "github.com/docker/distribution",
"InstalledVersion": "2.7.1+incompatible",
"FixedVersion": "v2.8.0",
Expand All @@ -43,6 +44,7 @@
},
{
"VulnerabilityID": "CVE-2022-23628",
"PkgID": "github.com/open-policy-agent/[email protected]",
"PkgName": "github.com/open-policy-agent/opa",
"InstalledVersion": "0.35.0",
"FixedVersion": "0.37.0",
Expand Down Expand Up @@ -81,6 +83,7 @@
},
{
"VulnerabilityID": "CVE-2021-38561",
"PkgID": "golang.org/x/[email protected]",
"PkgName": "golang.org/x/text",
"InstalledVersion": "0.3.6",
"FixedVersion": "0.3.7",
Expand Down Expand Up @@ -108,6 +111,7 @@
"Vulnerabilities": [
{
"VulnerabilityID": "GMS-2022-20",
"PkgID": "github.com/docker/[email protected]+incompatible",
"PkgName": "github.com/docker/distribution",
"InstalledVersion": "2.7.1+incompatible",
"FixedVersion": "v2.8.0",
Expand Down
5 changes: 5 additions & 0 deletions integration/testdata/gomod.json.golden
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"Vulnerabilities": [
{
"VulnerabilityID": "GMS-2022-20",
"PkgID": "github.com/docker/[email protected]+incompatible",
"PkgName": "github.com/docker/distribution",
"InstalledVersion": "2.7.1+incompatible",
"FixedVersion": "v2.8.0",
Expand All @@ -43,6 +44,7 @@
},
{
"VulnerabilityID": "CVE-2022-23628",
"PkgID": "github.com/open-policy-agent/[email protected]",
"PkgName": "github.com/open-policy-agent/opa",
"InstalledVersion": "0.35.0",
"FixedVersion": "0.37.0",
Expand Down Expand Up @@ -81,6 +83,7 @@
},
{
"VulnerabilityID": "CVE-2021-38561",
"PkgID": "golang.org/x/[email protected]",
"PkgName": "golang.org/x/text",
"InstalledVersion": "0.3.6",
"FixedVersion": "0.3.7",
Expand Down Expand Up @@ -108,6 +111,7 @@
"Vulnerabilities": [
{
"VulnerabilityID": "GMS-2022-20",
"PkgID": "github.com/docker/[email protected]+incompatible",
"PkgName": "github.com/docker/distribution",
"InstalledVersion": "2.7.1+incompatible",
"FixedVersion": "v2.8.0",
Expand Down Expand Up @@ -136,6 +140,7 @@
"Vulnerabilities": [
{
"VulnerabilityID": "GMS-2022-20",
"PkgID": "github.com/docker/[email protected]+incompatible",
"PkgName": "github.com/docker/distribution",
"InstalledVersion": "2.7.1+incompatible",
"FixedVersion": "v2.8.0",
Expand Down
158 changes: 108 additions & 50 deletions pkg/fanal/analyzer/language/golang/mod/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,19 @@ var (
)

type gomodAnalyzer struct {
// root go.mod/go.sum
modParser godeptypes.Parser
sumParser godeptypes.Parser

// go.mod/go.sum in dependencies
leafModParser godeptypes.Parser
}

func newGoModAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
return &gomodAnalyzer{
modParser: mod.NewParser(),
sumParser: sum.NewParser(),
modParser: mod.NewParser(true), // Only the root module should replace
sumParser: sum.NewParser(),
leafModParser: mod.NewParser(false),
}, nil
}

Expand Down Expand Up @@ -94,8 +99,8 @@ func (a *gomodAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalys
return nil, xerrors.Errorf("walk error: %w", err)
}

if err = fillLicenses(apps); err != nil {
return nil, xerrors.Errorf("unable to identify licenses: %w", err)
if err = a.fillAdditionalData(apps); err != nil {
return nil, xerrors.Errorf("unable to collect additional info: %w", err)
}

return &analyzer.AnalysisResult{
Expand All @@ -116,6 +121,97 @@ func (a *gomodAnalyzer) Version() int {
return version
}

// fillAdditionalData collects licenses and dependency relationships, then update applications.
func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application) error {
gopath := os.Getenv("GOPATH")
if gopath == "" {
gopath = build.Default.GOPATH
}

// $GOPATH/pkg/mod
modPath := filepath.Join(gopath, "pkg", "mod")
if !fsutils.DirExists(modPath) {
log.Logger.Debugf("GOPATH (%s) not found. Need 'go mod download' to fill licenses and dependency relationships", modPath)
return nil
}

licenses := map[string][]string{}
for i, app := range apps {
// Actually used dependencies
usedLibs := lo.SliceToMap(app.Libraries, func(pkg types.Package) (string, types.Package) {
return pkg.Name, pkg
})
for j, lib := range app.Libraries {
if l, ok := licenses[lib.ID]; ok {
// Fill licenses
apps[i].Libraries[j].Licenses = l
continue
}

// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/[email protected]
modDir := filepath.Join(modPath, fmt.Sprintf("%s@v%s", normalizeModName(lib.Name), lib.Version))

// Collect licenses
if licenseNames, err := findLicense(modDir); err != nil {
return xerrors.Errorf("license error: %w", err)
} else {
// Cache the detected licenses
licenses[lib.ID] = licenseNames

// Fill licenses
apps[i].Libraries[j].Licenses = licenseNames
}

// Collect dependencies of the direct dependency
if dep, err := a.collectDeps(modDir, lib.ID); err != nil {
return xerrors.Errorf("dependency graph error: %w", err)
} else if dep.ID == "" {
// go.mod not found
continue
} else {
// Filter out unused dependencies and convert module names to module IDs
apps[i].Libraries[j].DependsOn = lo.FilterMap(dep.DependsOn, func(modName string, _ int) (string, bool) {
if m, ok := usedLibs[modName]; !ok {
return "", false
} else {
return m.ID, true
}
})
}
}
}
return nil
}

func (a *gomodAnalyzer) collectDeps(modDir string, pkgID string) (godeptypes.Dependency, error) {
// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/[email protected]/go.mod
modPath := filepath.Join(modDir, "go.mod")
f, err := os.Open(modPath)
if errors.Is(err, fs.ErrNotExist) {
log.Logger.Debugf("Unable to identify dependencies of %s as it doesn't support Go modules", pkgID)
return godeptypes.Dependency{}, nil
} else if err != nil {
return godeptypes.Dependency{}, xerrors.Errorf("file open error: %w", err)
}
defer f.Close()

// Parse go.mod under $GOPATH/pkg/mod
libs, _, err := a.leafModParser.Parse(f)
if err != nil {
return godeptypes.Dependency{}, xerrors.Errorf("%s parse error: %w", modPath, err)
}

// Filter out indirect dependencies
dependsOn := lo.FilterMap(libs, func(lib godeptypes.Library, index int) (string, bool) {
return lib.Name, !lib.Indirect
})

return godeptypes.Dependency{
ID: pkgID,
DependsOn: dependsOn,
}, nil
}

func parse(fsys fs.FS, path string, parser godeptypes.Parser) (*types.Application, error) {
f, err := fsys.Open(path)
if err != nil {
Expand All @@ -133,6 +229,7 @@ func parse(fsys fs.FS, path string, parser godeptypes.Parser) (*types.Applicatio
if err != nil {
return nil, xerrors.Errorf("%s parse error: %w", path, err)
}

return language.ToApplication(types.GoModule, path, "", libs, deps), nil
}

Expand Down Expand Up @@ -171,51 +268,7 @@ func mergeGoSum(gomod, gosum *types.Application) {
gomod.Libraries = maps.Values(uniq)
}

func fillLicenses(apps []types.Application) error {
gopath := os.Getenv("GOPATH")
if gopath == "" {
gopath = build.Default.GOPATH
}

// $GOPATH/pkg/mod
modPath := filepath.Join(gopath, "pkg", "mod")
if !fsutils.DirExists(modPath) {
log.Logger.Debugf("GOPATH (%s) not found. Need 'go mod download' to fill license information", modPath)
return nil
}

licenses := map[string][]string{}
for i, app := range apps {
for j, lib := range app.Libraries {
libID := lib.Name + "@v" + lib.Version
if l, ok := licenses[libID]; ok {
// Fill licenses
apps[i].Libraries[j].Licenses = l
continue
}

// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/[email protected]
modDir := filepath.Join(modPath, fmt.Sprintf("%s@v%s", normalizeModName(lib.Name), lib.Version))
l, err := findLicense(modDir)
if err != nil {
return xerrors.Errorf("golang license error: %w", err)
} else if l == nil || len(l.Findings) == 0 {
continue
}
licenseNames := lo.Map(l.Findings, func(finding types.LicenseFinding, _ int) string {
return finding.Name
})
// Cache the detected licenses
licenses[libID] = licenseNames

// Fill licenses
apps[i].Libraries[j].Licenses = licenseNames
}
}
return nil
}

func findLicense(dir string) (*types.LicenseFile, error) {
func findLicense(dir string) ([]string, error) {
var license *types.LicenseFile
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
Expand Down Expand Up @@ -249,8 +302,13 @@ func findLicense(dir string) (*types.LicenseFile, error) {
return nil, nil
} else if err != nil && !errors.Is(err, io.EOF) {
return nil, fmt.Errorf("finding a known open source license: %w", err)
} else if license == nil || len(license.Findings) == 0 {
return nil, nil
}
return license, nil

return lo.Map(license.Findings, func(finding types.LicenseFinding, _ int) string {
return finding.Name
}), nil
}

// normalizeModName escapes upper characters
Expand Down
11 changes: 11 additions & 0 deletions pkg/fanal/analyzer/language/golang/mod/mod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@ func Test_gomodAnalyzer_Analyze(t *testing.T) {
FilePath: "go.mod",
Libraries: []types.Package{
{
ID: "github.com/aquasecurity/[email protected]",
Name: "github.com/aquasecurity/go-dep-parser",
Version: "0.0.0-20220406074731-71021a481237",
Licenses: []string{
"MIT",
},
DependsOn: []string{
"golang.org/x/[email protected]",
},
},
{
ID: "golang.org/x/[email protected]",
Name: "golang.org/x/xerrors",
Version: "0.0.0-20200804184101-5ec99f83aff1",
Indirect: true,
Expand All @@ -56,10 +61,15 @@ func Test_gomodAnalyzer_Analyze(t *testing.T) {
FilePath: "go.mod",
Libraries: []types.Package{
{
ID: "github.com/aquasecurity/[email protected]",
Name: "github.com/aquasecurity/go-dep-parser",
Version: "0.0.0-20230219131432-590b1dfb6edd",
DependsOn: []string{
"github.com/BurntSushi/[email protected]",
},
},
{
ID: "github.com/BurntSushi/[email protected]",
Name: "github.com/BurntSushi/toml",
Version: "0.3.1",
Indirect: true,
Expand All @@ -82,6 +92,7 @@ func Test_gomodAnalyzer_Analyze(t *testing.T) {
FilePath: "go.mod",
Libraries: []types.Package{
{
ID: "github.com/aquasecurity/[email protected]",
Name: "github.com/aquasecurity/go-dep-parser",
Version: "0.0.0-20211110174639-8257534ffed3",
},
Expand Down
Loading