2024-02-19 15:16:35 +04:00
package binary
import (
"debug/buildinfo"
2024-10-31 11:33:22 +05:30
"fmt"
2024-05-02 01:33:13 -04:00
"runtime/debug"
2024-05-24 15:17:48 +06:00
"slices"
2024-04-22 17:58:44 +06:00
"sort"
2024-02-19 15:16:35 +04:00
"strings"
2025-02-13 14:16:14 +06:00
"github.com/mattn/go-shellwords"
2024-11-21 20:05:16 +09:00
"github.com/samber/lo"
2024-05-02 01:33:13 -04:00
"github.com/spf13/pflag"
"golang.org/x/mod/semver"
2024-02-19 15:16:35 +04:00
"golang.org/x/xerrors"
2024-11-21 20:05:16 +09:00
"github.com/aquasecurity/trivy/pkg/dependency"
2024-05-07 16:25:52 +04:00
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
2024-04-22 17:58:44 +06:00
"github.com/aquasecurity/trivy/pkg/log"
2024-02-26 09:55:15 +04:00
xio "github.com/aquasecurity/trivy/pkg/x/io"
2024-02-19 15:16:35 +04:00
)
var (
ErrUnrecognizedExe = xerrors . New ( "unrecognized executable format" )
ErrNonGoBinary = xerrors . New ( "non go binary" )
)
// convertError detects buildinfo.errUnrecognizedFormat and convert to
// ErrUnrecognizedExe and convert buildinfo.errNotGoExe to ErrNonGoBinary
func convertError ( err error ) error {
errText := err . Error ( )
if strings . HasSuffix ( errText , "unrecognized file format" ) {
return ErrUnrecognizedExe
}
if strings . HasSuffix ( errText , "not a Go executable" ) {
return ErrNonGoBinary
}
return err
}
2024-04-22 17:58:44 +06:00
type Parser struct {
logger * log . Logger
}
2024-02-19 15:16:35 +04:00
2024-05-07 16:25:52 +04:00
func NewParser ( ) * Parser {
2024-04-22 17:58:44 +06:00
return & Parser {
logger : log . WithPrefix ( "gobinary" ) ,
}
2024-02-19 15:16:35 +04:00
}
// Parse scans file to try to report the Go and module versions.
2024-05-07 16:25:52 +04:00
func ( p * Parser ) Parse ( r xio . ReadSeekerAt ) ( [ ] ftypes . Package , [ ] ftypes . Dependency , error ) {
2024-02-19 15:16:35 +04:00
info , err := buildinfo . Read ( r )
if err != nil {
return nil , nil , convertError ( err )
}
2024-05-15 23:03:41 -07:00
// Ex: "go1.22.3 X:boringcrypto"
stdlibVersion := strings . TrimPrefix ( info . GoVersion , "go" )
stdlibVersion , _ , _ = strings . Cut ( stdlibVersion , " " )
2024-10-31 11:33:22 +05:30
// Add the `v` prefix to be consistent with module and dependency versions.
stdlibVersion = fmt . Sprintf ( "v%s" , stdlibVersion )
2024-05-15 23:03:41 -07:00
2024-05-02 01:33:13 -04:00
ldflags := p . ldFlags ( info . Settings )
2024-05-07 16:25:52 +04:00
pkgs := make ( ftypes . Packages , 0 , len ( info . Deps ) + 2 )
2024-05-17 13:43:56 +06:00
pkgs = append ( pkgs , ftypes . Package {
// Add the Go version used to build this binary.
2024-11-21 20:05:16 +09:00
ID : dependency . ID ( ftypes . GoBinary , "stdlib" , stdlibVersion ) ,
2024-05-17 13:43:56 +06:00
Name : "stdlib" ,
Version : stdlibVersion ,
Relationship : ftypes . RelationshipDirect , // Considered a direct dependency as the main module depends on the standard packages.
} )
2024-02-19 15:16:35 +04:00
for _ , dep := range info . Deps {
// binaries with old go version may incorrectly add module in Deps
// In this case Path == "", Version == "Devel"
// we need to skip this
if dep . Path == "" {
continue
}
mod := dep
if dep . Replace != nil {
mod = dep . Replace
}
2024-11-21 20:05:16 +09:00
version := p . checkVersion ( mod . Path , mod . Version )
2024-05-07 16:25:52 +04:00
pkgs = append ( pkgs , ftypes . Package {
2024-11-21 20:05:16 +09:00
ID : dependency . ID ( ftypes . GoBinary , mod . Path , version ) ,
Name : mod . Path ,
Version : version ,
Relationship : ftypes . RelationshipUnknown ,
} )
}
// There are times when gobinaries don't contain Main information.
// e.g. `Go` binaries (e.g. `go`, `gofmt`, etc.)
var deps [ ] ftypes . Dependency
if info . Main . Path != "" {
// Only binaries installed with `go install` contain semver version of the main module.
// Other binaries use the `(devel)` version, but still may contain a stamped version
// set via `go build -ldflags='-X main.version=<semver>'`, so we fallback to this as.
// as a secondary source.
// See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477.
2025-02-24 12:22:13 +01:00
version := p . checkVersion ( info . Main . Path , info . Main . Version )
ldflagsVersion := p . ParseLDFlags ( info . Main . Path , ldflags )
if version == "" || ( strings . HasPrefix ( version , "v0.0.0" ) && ldflagsVersion != "" ) {
version = ldflagsVersion
}
2024-11-21 20:05:16 +09:00
root := ftypes . Package {
ID : dependency . ID ( ftypes . GoBinary , info . Main . Path , version ) ,
Name : info . Main . Path ,
Version : version ,
Relationship : ftypes . RelationshipRoot ,
}
depIDs := lo . Map ( pkgs , func ( pkg ftypes . Package , _ int ) string {
return pkg . ID
2024-02-19 15:16:35 +04:00
} )
2024-11-21 20:05:16 +09:00
sort . Strings ( depIDs )
deps = [ ] ftypes . Dependency {
{
ID : root . ID ,
DependsOn : depIDs , // Consider all packages as dependencies of the main module.
} ,
}
// Add main module
pkgs = append ( pkgs , root )
2024-02-19 15:16:35 +04:00
}
2024-05-07 16:25:52 +04:00
sort . Sort ( pkgs )
2024-11-21 20:05:16 +09:00
return pkgs , deps , nil
2024-02-19 15:16:35 +04:00
}
2024-04-22 17:58:44 +06:00
// checkVersion detects `(devel)` versions, removes them and adds a debug message about it.
func ( p * Parser ) checkVersion ( name , version string ) string {
if version == "(devel)" {
2024-05-02 01:33:13 -04:00
p . logger . Debug ( "Unable to detect main module's dependency version - `(devel)` is used" , log . String ( "dependency" , name ) )
2024-04-22 17:58:44 +06:00
return ""
}
return version
}
2024-05-02 01:33:13 -04:00
func ( p * Parser ) ldFlags ( settings [ ] debug . BuildSetting ) [ ] string {
for _ , setting := range settings {
if setting . Key != "-ldflags" {
continue
}
2025-02-13 14:16:14 +06:00
flags , err := shellwords . Parse ( setting . Value )
if err != nil {
p . logger . Error ( "Could not parse -ldflags found in build info" , log . Err ( err ) )
return nil
}
return flags
2024-05-02 01:33:13 -04:00
}
return nil
}
// ParseLDFlags attempts to parse the binary's version from any `-ldflags` passed to `go build` at build time.
func ( p * Parser ) ParseLDFlags ( name string , flags [ ] string ) string {
p . logger . Debug ( "Parsing dependency's build info settings" , "dependency" , name , "-ldflags" , flags )
fset := pflag . NewFlagSet ( "ldflags" , pflag . ContinueOnError )
// This prevents the flag set from erroring out if other flags were provided.
// This helps keep the implementation small, so that only the -X flag is needed.
fset . ParseErrorsWhitelist . UnknownFlags = true
// The shorthand name is needed here because setting the full name
// to `X` will cause the flag set to look for `--X` instead of `-X`.
// The flag can also be set multiple times, so a string slice is needed
// to handle that edge case.
var x map [ string ] string
fset . StringToStringVarP ( & x , "" , "X" , nil , "" )
2025-02-13 14:16:14 +06:00
// Init `help` flag to avoid error in flags with `h` (e.g. `-lpthread`)
fset . BoolP ( "help" , "h" , false , "just to disable the built-in help flag" )
2024-05-02 01:33:13 -04:00
if err := fset . Parse ( flags ) ; err != nil {
p . logger . Error ( "Could not parse -ldflags found in build info" , log . Err ( err ) )
return ""
}
2024-05-24 15:17:48 +06:00
// foundVersions contains discovered versions by type.
// foundVersions doesn't contain duplicates. Versions are filled into first corresponding category.
// Possible elements(categories):
// [0]: Versions using format `github.com/<module_owner>/<module_name>/cmd/**/*.<version>=x.x.x`
// [1]: Versions that use prefixes from `defaultPrefixes`
// [2]: Other versions
var foundVersions = make ( [ ] [ ] string , 3 )
2024-11-21 20:05:16 +09:00
defaultPrefixes := [ ] string {
"main" ,
"common" ,
"version" ,
"cmd" ,
}
2024-05-02 01:33:13 -04:00
for key , val := range x {
// It's valid to set the -X flags with quotes so we trim any that might
// have been provided: Ex:
//
// -X main.version=1.0.0
// -X=main.version=1.0.0
// -X 'main.version=1.0.0'
// -X='main.version=1.0.0'
// -X="main.version=1.0.0"
// -X "main.version=1.0.0"
key = strings . TrimLeft ( key , ` ' ` )
val = strings . TrimRight ( val , ` ' ` )
2024-05-24 15:17:48 +06:00
if isVersionXKey ( key ) && isValidSemVer ( val ) {
switch {
case strings . HasPrefix ( key , name + "/cmd/" ) :
foundVersions [ 0 ] = append ( foundVersions [ 0 ] , val )
case slices . Contains ( defaultPrefixes , strings . ToLower ( versionPrefix ( key ) ) ) :
foundVersions [ 1 ] = append ( foundVersions [ 1 ] , val )
default :
foundVersions [ 2 ] = append ( foundVersions [ 2 ] , val )
}
2024-05-02 01:33:13 -04:00
}
}
2024-05-24 15:17:48 +06:00
return p . chooseVersion ( name , foundVersions )
}
// chooseVersion chooses version from found versions
// Categories order:
// module name with `cmd` => versions with default prefixes => other versions
// See more in https://github.com/aquasecurity/trivy/issues/6702#issuecomment-2122271427
func ( p * Parser ) chooseVersion ( moduleName string , vers [ ] [ ] string ) string {
for _ , versions := range vers {
// Versions for this category was not found
if len ( versions ) == 0 {
continue
}
// More than 1 version for one category.
// Use empty version.
if len ( versions ) > 1 {
p . logger . Debug ( "Unable to detect dependency version. `-ldflags` build info settings contain more than one version. Empty version used." , log . String ( "dependency" , moduleName ) )
return ""
}
return versions [ 0 ]
}
p . logger . Debug ( "Unable to detect dependency version. `-ldflags` build info settings don't contain version flag. Empty version used." , log . String ( "dependency" , moduleName ) )
2024-05-02 01:33:13 -04:00
return ""
}
2024-05-24 15:17:48 +06:00
func isVersionXKey ( key string ) bool {
2024-05-02 01:33:13 -04:00
key = strings . ToLower ( key )
// The check for a 'ver' prefix enables the parser to pick up Trivy's own version value that's set.
2024-05-17 13:55:24 +06:00
return strings . HasSuffix ( key , ".version" ) || strings . HasSuffix ( key , ".ver" )
2024-05-02 01:33:13 -04:00
}
func isValidSemVer ( ver string ) bool {
// semver.IsValid strictly checks for the v prefix so prepending 'v'
// here and checking validity again increases the chances that we
// parse a valid semver version.
return semver . IsValid ( ver ) || semver . IsValid ( "v" + ver )
}
2024-05-24 15:17:48 +06:00
// versionPrefix returns version prefix from `-ldflags` flag key
// e.g.
2024-06-10 11:05:03 +04:00
// - `github.com/aquasecurity/trivy/pkg/version/app.ver` => `version`
2024-05-24 15:17:48 +06:00
// - `github.com/google/go-containerregistry/cmd/crane/common.ver` => `common`
func versionPrefix ( s string ) string {
// Trim module part.
// e.g. `github.com/aquasecurity/trivy/pkg/Version.version` => `Version.version`
if lastIndex := strings . LastIndex ( s , "/" ) ; lastIndex > 0 {
s = s [ lastIndex + 1 : ]
}
s , _ , _ = strings . Cut ( s , "." )
return strings . ToLower ( s )
}