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

Go source cataloger #3452

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ require (
github.com/adrg/xdg v0.5.3
github.com/magiconair/properties v1.8.7
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/tools v0.23.0
)

require (
Expand Down
5 changes: 5 additions & 0 deletions internal/task/package_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ func DefaultPackageTaskFactories() PackageTaskFactories {
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Go, Golang, "gomod",
),
newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
return golang.NewGoModuleSourceFileCataloger(cfg.PackagesConfig.Golang)
},
pkgcataloging.InstalledTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Go, Golang, "gosource"),
newSimplePackageTaskFactory(java.NewGradleLockfileCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Java, "gradle"),
newPackageTaskFactory(
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
Expand Down
13 changes: 11 additions & 2 deletions syft/pkg/cataloger/golang/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import (
var versionCandidateGroups = regexp.MustCompile(`(?P<version>\d+(\.\d+)?(\.\d+)?)(?P<candidate>\w*)`)

const (
modFileCatalogerName = "go-module-file-cataloger"
binaryCatalogerName = "go-module-binary-cataloger"
modFileCatalogerName = "go-module-file-cataloger"
binaryCatalogerName = "go-module-binary-cataloger"
sourceFileCatalogerName = "go-module-source-file-cataloger"
)

// NewGoModuleFileCataloger returns a new cataloger object that searches within go.mod files.
Expand All @@ -33,3 +34,11 @@ func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger {
).
WithProcessors(stdlibProcessor)
}

// NewGoModuleSourceFileCataloger returns a new cataloger object that uses the go.mod file
// to extract the module name and then searches all direct and transitive dependencies
// for the given module source tree
func NewGoModuleSourceFileCataloger(opts CatalogerConfig) pkg.Cataloger {
return generic.NewCataloger(sourceFileCatalogerName).
WithParserByGlobs(newGoModSourceCataloger(opts).parseGoModFile, "**/go.mod")
}
93 changes: 93 additions & 0 deletions syft/pkg/cataloger/golang/parse_go_mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"bufio"
"context"
"fmt"
"golang.org/x/tools/go/packages"
"io"
"sort"
"strings"
Expand All @@ -19,16 +20,108 @@
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)

var searchSuffix = "/..."

type goModCataloger struct {
licenseResolver goLicenseResolver
}

type goModSourceCataloger struct{}

func newGoModCataloger(opts CatalogerConfig) *goModCataloger {
return &goModCataloger{
licenseResolver: newGoLicenseResolver(modFileCatalogerName, opts),
}
}

func newGoModSourceCataloger(opts CatalogerConfig) *goModSourceCataloger {

Check failure on line 37 in syft/pkg/cataloger/golang/parse_go_mod.go

View workflow job for this annotation

GitHub Actions / Static analysis

unused-parameter: parameter 'opts' seems to be unused, consider removing or renaming it as _ (revive)
return &goModSourceCataloger{}
}

func (c *goModSourceCataloger) parseGoModFile(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {

Check failure on line 41 in syft/pkg/cataloger/golang/parse_go_mod.go

View workflow job for this annotation

GitHub Actions / Static analysis

unused-parameter: parameter 'resolver' seems to be unused, consider removing or renaming it as _ (revive)
contents, err := io.ReadAll(reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to read module file: %w", err)
}

file, err := modfile.Parse("go.mod", contents, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse module file: %w", err)
}

// extract the module name and add the search suffix
mainModuleName := file.Module.Mod.Path
mainModuleName = fmt.Sprintf("%s%s", mainModuleName, searchSuffix)

cfg := &packages.Config{
Context: ctx,
Mode: packages.NeedImports | packages.NeedDeps | packages.NeedFiles | packages.NeedName | packages.NeedModule,
Tests: true,
}

rootPkgs, err := packages.Load(cfg, mainModuleName)
if err != nil {
return nil, nil, fmt.Errorf("failed to load packages for %s: %w", mainModuleName, err)
}

syftPackages := make([]pkg.Package, 0)
pkgErrorOccurred := false
otherErrorOccurred := false
packages.Visit(rootPkgs, func(p *packages.Package) bool {
if len(p.Errors) > 0 {
pkgErrorOccurred = true
return false
}
if p.Module == nil {
otherErrorOccurred = true
return false
}

if !isValid(p) {
return false
}

syftPackages = append(syftPackages, pkg.Package{
Name: p.Name,
Version: p.Module.Version,
// Licenses (TODO)
// Locations: file.NewLocationSet(reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
PURL: packageURL(p.PkgPath, p.Module.Version),
Language: pkg.Go,
Type: pkg.GoModulePkg,
Metadata: pkg.GolangModuleEntryMetadata{
Path: p.Module.Path,
Version: p.Module.Version,
Replace: p.Module.Replace,
Time: p.Module.Time,
Main: p.Module.Main,
Indirect: p.Module.Indirect,
Dir: p.Module.Dir,
GoMod: p.Module.GoMod,
GoVersion: p.Module.GoVersion,
},
})
return true
}, nil)
if pkgErrorOccurred {

Check failure on line 106 in syft/pkg/cataloger/golang/parse_go_mod.go

View workflow job for this annotation

GitHub Actions / Static analysis

empty-block: this block is empty, you can remove it (revive)

Check failure on line 106 in syft/pkg/cataloger/golang/parse_go_mod.go

View workflow job for this annotation

GitHub Actions / Static analysis

SA9003: empty branch (staticcheck)
// TODO: log error as warning for packages that could not be analyzed
}
if otherErrorOccurred {

Check failure on line 109 in syft/pkg/cataloger/golang/parse_go_mod.go

View workflow job for this annotation

GitHub Actions / Static analysis

empty-block: this block is empty, you can remove it (revive)

Check failure on line 109 in syft/pkg/cataloger/golang/parse_go_mod.go

View workflow job for this annotation

GitHub Actions / Static analysis

SA9003: empty branch (staticcheck)
// TODO: log errors for direct/transitive dependency loading
}
return syftPackages, nil, nil
}

func isValid(p *packages.Package) bool {
if p.Name == "" {
return false
}
if p.Module.Version == "" {
return false
}
return true
}

// parseGoModFile takes a go.mod and lists all packages discovered.
//
//nolint:funlen
Expand Down
19 changes: 19 additions & 0 deletions syft/pkg/golang.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package pkg

import (
"golang.org/x/tools/go/packages"
"time"
)

// GolangBinaryBuildinfoEntry represents all captured data for a Golang binary
type GolangBinaryBuildinfoEntry struct {
BuildSettings KeyValues `json:"goBuildSettings,omitempty" cyclonedx:"goBuildSettings"`
Expand All @@ -15,3 +20,17 @@ type GolangBinaryBuildinfoEntry struct {
type GolangModuleEntry struct {
H1Digest string `json:"h1Digest,omitempty" cyclonedx:"h1Digest"`
}

// GolangModuleEntryMetadata represetns all captured data from the golang.org/x/tools/go/packages package
// when scanning a golang source directory for direct and indirect dependencies
type GolangModuleEntryMetadata struct {
Path string // module path
Version string // module version
Replace *packages.Module // replaced by this module
Time *time.Time // time version was created
Main bool // is this the main module?
Indirect bool // is this module only an indirect dependency of main module?
Dir string // directory holding files for this module, if any
GoMod string // path to go.mod file used when loading this module, if any
GoVersion string // go version used in module
}
Loading