Skip to content

Commit

Permalink
feat(go): use toolchain as stdlib version for go.mod files (#7163)
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitriyLewen authored Sep 3, 2024
1 parent f80183c commit 2d80769
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 96 deletions.
21 changes: 20 additions & 1 deletion docs/docs/coverage/language/golang.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The table below provides an outline of the features Trivy offers.

| Artifact | Offline[^1] | Dev dependencies | [Dependency graph][dependency-graph] | Stdlib | [Detection Priority][detection-priority] |
|----------|:-----------:|:-----------------|:------------------------------------:|:------:|:----------------------------------------:|
| Modules || Include |[^2] | - | - |
| Modules || Include |[^2] | [^6] | [](#stdlib) |
| Binaries || Exclude | - |[^4] | Not needed |

!!! note
Expand Down Expand Up @@ -65,6 +65,23 @@ To identify licenses and dependency relationships, you need to download modules
such as `go mod download`, `go mod tidy`, etc.
Trivy traverses `$GOPATH/pkg/mod` and collects those extra information.

#### stdlib
If [--detection-priority comprehensive][detection-priority] is passed, Trivy determines the minimum version of `Go` and saves it as a `stdlib` dependency.

By default, `Go` selects the higher version from of `toolchan` or local version of `Go`.
See [toolchain] for more details.

To obtain reproducible scan results Trivy doesn't check the local version of `Go`.
Trivy shows the minimum required version for the `go.mod` file, obtained from `toolchain` line (or from the `go` line, if `toolchain` line is omitted).

!!! note
Trivy detects `stdlib` only for `Go` 1.21 or higher.

The version from the `go` line (for `Go` 1.20 or early) is not a minimum required version.
For details, see [this](https://go.googlesource.com/proposal/+/master/design/57001-gotoolchain.md).



### Go binaries
Trivy scans binaries built by Go, which include [module information](https://tip.golang.org/doc/go1.18#go-version).
If there is a Go binary in your container image, Trivy automatically finds and scans it.
Expand Down Expand Up @@ -93,6 +110,8 @@ empty if it cannot do so[^5]. For the second case, the version of such packages
[^3]: See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477
[^4]: Identify the Go version used to compile the binary and detect its vulnerabilities
[^5]: See https://github.com/golang/go/issues/63432#issuecomment-1751610604
[^6]: Only available if `toolchain` directive exists

[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies
[toolchain]: https://go.dev/doc/toolchain
[detection-priority]: ../../scanner/vulnerability.md#detection-priority
81 changes: 74 additions & 7 deletions pkg/dependency/parser/golang/mod/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ var (
)

type Parser struct {
replace bool // 'replace' represents if the 'replace' directive should be taken into account.
replace bool // 'replace' represents if the 'replace' directive should be taken into account.
useMinVersion bool
}

func NewParser(replace bool) *Parser {
func NewParser(replace, useMinVersion bool) *Parser {
return &Parser{
replace: replace,
replace: replace,
useMinVersion: useMinVersion,
}
}

Expand Down Expand Up @@ -80,7 +82,20 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc

skipIndirect := true
if modFileParsed.Go != nil { // Old go.mod file may not include the go version. Go version for these files is less than 1.17
skipIndirect = lessThan117(modFileParsed.Go.Version)
skipIndirect = lessThan(modFileParsed.Go.Version, 1, 17)
}

// Use minimal required go version from `toolchain` line (or from `go` line if `toolchain` is omitted) as `stdlib`.
// Show `stdlib` only with `useMinVersion` flag.
if p.useMinVersion {
if toolchainVer := toolchainVersion(modFileParsed.Toolchain, modFileParsed.Go); toolchainVer != "" {
pkgs["stdlib"] = ftypes.Package{
ID: packageID("stdlib", toolchainVer),
Name: "stdlib",
Version: toolchainVer,
Relationship: ftypes.RelationshipDirect, // Considered a direct dependency as the main module depends on the standard packages.
}
}
}

// Main module
Expand Down Expand Up @@ -150,8 +165,12 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
return lo.Values(pkgs), nil, nil
}

// Check if the Go version is less than 1.17
func lessThan117(ver string) bool {
// lessThan checks if the Go version is less than `<majorVer>.<minorVer>`
func lessThan(ver string, majorVer, minorVer int) bool {
if ver == "" {
return false
}

ss := strings.Split(ver, ".")
if len(ss) != 2 {
return false
Expand All @@ -165,7 +184,55 @@ func lessThan117(ver string) bool {
return false
}

return major <= 1 && minor < 17
return major <= majorVer && minor < minorVer
}

// toolchainVersion returns version from `toolchain`.
// If `toolchain` is omitted - return version from `go` line (if it is version in toolchain format)
// cf. https://go.dev/doc/toolchain
func toolchainVersion(toolchain *modfile.Toolchain, goVer *modfile.Go) string {
if toolchain != nil && toolchain.Name != "" {
// cf. https://go.dev/doc/toolchain#name
// `dropping the initial go and discarding off any suffix beginning with -`
// e.g. `go1.22.5-custom` => `1.22.5`
name, _, _ := strings.Cut(toolchain.Name, "-")
return strings.TrimPrefix(name, "go")
}

if goVer != nil {
return toolchainVersionFromGoLine(goVer.Version)
}
return ""
}

// toolchainVersionFromGoLine detects Go version from `go` line if `toolchain` line is omitted.
// `go` line supports the following formats:
// cf. https://go.dev/doc/toolchain#version
// - `1.N.P`. e.g. `1.22.0`
// - `1.N`. e.g. `1.22`
// - `1.NrcR`. e.g. `1.22rc1`
// - `1.NbetaR`. e.g. `1.18beta1` - only for Go 1.20 or earlier
func toolchainVersionFromGoLine(ver string) string {
var majorMinorVer string

if ss := strings.Split(ver, "."); len(ss) > 2 { // `1.N.P`
majorMinorVer = strings.Join(ss[:2], ".")
} else if v, _, rcFound := strings.Cut(ver, "rc"); rcFound { // `1.NrcR`
majorMinorVer = v
} else { // `1.N`
majorMinorVer = ver
// Add `.0` suffix to avoid user confusing.
// See https://github.com/aquasecurity/trivy/pull/7163#discussion_r1682424315
ver = v + ".0"
}

// `toolchain` has been added in go 1.21.
// So we need to check that Go version is 1.21 or higher.
// cf. https://github.com/aquasecurity/trivy/pull/7163#discussion_r1682424315
if lessThan(majorMinorVer, 1, 21) {
return ""
}
return ver
}

func packageID(name, version string) string {
Expand Down
108 changes: 102 additions & 6 deletions pkg/dependency/parser/golang/mod/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,31 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/mod/modfile"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
)

func TestParse(t *testing.T) {
tests := []struct {
name string
file string
replace bool
want []ftypes.Package
name string
file string
replace bool
useMinVersion bool
want []ftypes.Package
}{
{
name: "normal with stdlib",
file: "testdata/normal/go.mod",
replace: true,
useMinVersion: true,
want: GoModNormal,
},
{
name: "normal",
file: "testdata/normal/go.mod",
replace: true,
want: GoModNormal,
want: GoModNormalWithoutStdlib,
},
{
name: "without go version",
Expand Down Expand Up @@ -85,7 +94,7 @@ func TestParse(t *testing.T) {
f, err := os.Open(tt.file)
require.NoError(t, err)

got, _, err := NewParser(tt.replace).Parse(f)
got, _, err := NewParser(tt.replace, tt.useMinVersion).Parse(f)
require.NoError(t, err)

sort.Sort(ftypes.Packages(got))
Expand All @@ -95,3 +104,90 @@ func TestParse(t *testing.T) {
})
}
}

func TestToolchainVersion(t *testing.T) {
tests := []struct {
name string
modFile modfile.File
want string
}{
{
name: "version from toolchain line",
modFile: modfile.File{
Toolchain: &modfile.Toolchain{
Name: "1.21.1",
},
},
want: "1.21.1",
},
{
name: "version from toolchain line with suffix",
modFile: modfile.File{
Toolchain: &modfile.Toolchain{
Name: "1.21.1-custom",
},
},
want: "1.21.1",
},
{
name: "'1.18rc1' from go line",
modFile: modfile.File{
Go: &modfile.Go{
Version: "1.18rc1",
},
},
want: "",
},
{
name: "'1.18.1' from go line",
modFile: modfile.File{
Go: &modfile.Go{
Version: "1.18.1",
},
},
want: "",
},
{
name: "'1.20' from go line",
modFile: modfile.File{
Go: &modfile.Go{
Version: "1.20",
},
},
want: "",
},
{
name: "'1.21' from go line",
modFile: modfile.File{
Go: &modfile.Go{
Version: "1.21",
},
},
want: "1.21.0",
},
{
name: "'1.21rc1' from go line",
modFile: modfile.File{
Go: &modfile.Go{
Version: "1.21rc1",
},
},
want: "1.21rc1",
},
{
name: "'1.21.2' from go line",
modFile: modfile.File{
Go: &modfile.Go{
Version: "1.21.2",
},
},
want: "1.21.2",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, toolchainVersion(tt.modFile.Toolchain, tt.modFile.Go))
})
}
}
62 changes: 50 additions & 12 deletions pkg/dependency/parser/golang/mod/parse_testcase.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package mod

import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
import (
"slices"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
)

var (
// execute go mod tidy in normal folder
Expand All @@ -17,37 +21,71 @@ var (
},
},
{
ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff",
Name: "github.com/aquasecurity/go-dep-parser",
Version: "0.0.0-20211224170007-df43bca6b6ff",
ID: "stdlib@v1.22.5",
Name: "stdlib",
Version: "1.22.5",
Relationship: ftypes.RelationshipDirect,
},
{
ID: "github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d",
Name: "github.com/aquasecurity/go-version",
Version: "0.0.0-20240603093900-cf8a8d29271d",
Relationship: ftypes.RelationshipDirect,
ExternalReferences: []ftypes.ExternalRef{
{
Type: ftypes.RefVCS,
URL: "https://github.com/aquasecurity/go-dep-parser",
URL: "https://github.com/aquasecurity/go-version",
},
},
},
{
ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1",
Name: "golang.org/x/xerrors",
Version: "0.0.0-20200804184101-5ec99f83aff1",
ID: "github.com/davecgh/go-spew@v1.1.2-0.20180830191138-d8f796af33cc",
Name: "github.com/davecgh/go-spew",
Version: "1.1.2-0.20180830191138-d8f796af33cc",
Relationship: ftypes.RelationshipIndirect,
ExternalReferences: []ftypes.ExternalRef{
{
Type: ftypes.RefVCS,
URL: "https://github.com/davecgh/go-spew",
},
},
},
{
ID: "github.com/pmezard/go-difflib@v1.0.1-0.20181226105442-5d4384ee4fb2",
Name: "github.com/pmezard/go-difflib",
Version: "1.0.1-0.20181226105442-5d4384ee4fb2",
Relationship: ftypes.RelationshipIndirect,
ExternalReferences: []ftypes.ExternalRef{
{
Type: ftypes.RefVCS,
URL: "https://github.com/pmezard/go-difflib",
},
},
},
{
ID: "gopkg.in/yaml.v3@v3.0.0-20210107192922-496545a6307b",
Name: "gopkg.in/yaml.v3",
Version: "3.0.0-20210107192922-496545a6307b",
ID: "github.com/stretchr/testify@v1.9.0",
Name: "github.com/stretchr/testify",
Version: "1.9.0",
Relationship: ftypes.RelationshipIndirect,
ExternalReferences: []ftypes.ExternalRef{
{
Type: ftypes.RefVCS,
URL: "https://github.com/go-yaml/yaml",
URL: "https://github.com/stretchr/testify",
},
},
},
{
ID: "golang.org/x/xerrors@v0.0.0-20231012003039-104605ab7028",
Name: "golang.org/x/xerrors",
Version: "0.0.0-20231012003039-104605ab7028",
Relationship: ftypes.RelationshipIndirect,
},
}

GoModNormalWithoutStdlib = slices.DeleteFunc(slices.Clone(GoModNormal), func(f ftypes.Package) bool {
return f.Name == "stdlib"
})

// execute go mod tidy in replaced folder
GoModReplaced = []ftypes.Package{
{
Expand Down
Loading

0 comments on commit 2d80769

Please sign in to comment.