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): use toolchain as stdlib version for go.mod files #7163

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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