Skip to content

fix: merge go.sum into go.mod for Go <1.17 projects #2479

@kotakanbe

Description

@kotakanbe

Summary

For Go projects using Go <1.17, `go.mod` does not include indirect dependencies. Trivy's gomod analyzer is designed to merge adjacent `go.sum` into `go.mod` results to capture these indirect deps. However, this merge has never worked in Vuls because `scanLibraries()` processes each lockfile individually — the fanal PostAnalyzer's filesystem walk cannot find `go.sum` when only `go.mod` is passed.

This is a pre-existing bug (present in master), not a regression from #2476.

Impact

  • Go <1.17 projects: indirect dependencies from `go.sum` are missing from scan results, potentially causing missed vulnerability detections
  • Go 1.17+: no impact (indirect deps are listed in `go.mod`)

Root cause

`scanLibraries()` calls `AnalyzeLibrary()` per file. The fanal gomod PostAnalyzer expects both `go.mod` and `go.sum` to be present in its virtual filesystem, but only the single file being analyzed is copied there.

Fix plan

After #2476 is merged, modify `scanLibraries()` in `scanner/base.go` (and similarly in `scanner/pseudo.go` and `scanner/windows.go`).

Step 1: Buffer go.sum in scanLibraries

In the file processing loop, when a `go.sum` is encountered, save its contents keyed by directory instead of passing to `AnalyzeLibrary`:

```go
goSumContents := map[string][]byte{} // dir -> contents

for _, path := range detectFiles {
// ... existing path resolution and file reading ...

if filepath.Base(abspath) == "go.sum" {
    dir := filepath.Dir(abspath)
    goSumContents[dir] = contents
    continue
}

// ... existing AnalyzeLibrary call ...

}
```

Step 2: After loop, merge go.sum into go.mod results

```go
for i, scanner := range l.LibraryScanners {
if scanner.Type != ftypes.GoModule {
continue
}
if !lessThanGo117(scanner) {
continue
}
dir := filepath.Dir(scanner.LockfilePath)
sumContents, ok := goSumContents[dir]
if !ok {
continue
}
mergeGoSum(&l.LibraryScanners[i], sumContents)
}
```

Step 3: Implement lessThanGo117

Reference: `pkg/fanal/analyzer/language/golang/mod/mod.go` line 268-276

```go
func lessThanGo117(scanner models.LibraryScanner) bool {
// In Go 1.17+, go.mod includes "// indirect" comments which Trivy
// parses into Relationship=RelationshipIndirect. If none are found,
// the project is likely Go <1.17.
for _, lib := range scanner.Libs {
if lib.Indirect {
return false
}
}
return true
}
```

Note: `models.Library` currently does not have an `Indirect` field. This needs to be added, or the check needs to use the raw `ftypes.Package.Relationship` before conversion to `models.Library`. The simpler approach is to check at the `ftypes.Application` level before `convertLibWithScanner`.

Step 4: Implement mergeGoSum

Reference: `pkg/fanal/analyzer/language/golang/mod/mod.go` line 279-302

```go
func mergeGoSum(scanner *models.LibraryScanner, sumContents []byte) {
sumParser := sum.NewParser()
r := xio.NopCloser(bytes.NewReader(sumContents))
sumPkgs, _, err := sumParser.Parse(context.Background(), r)
if err != nil {
return // best effort
}

existing := map[string]bool{}
for _, lib := range scanner.Libs {
    existing[lib.Name] = true
}

for _, pkg := range sumPkgs {
    if existing[pkg.Name] {
        continue
    }
    scanner.Libs = append(scanner.Libs, models.Library{
        Name:    pkg.Name,
        Version: pkg.Version,
        PURL:    newPURL(ftypes.GoModule, types.Metadata{}, pkg),
    })
}

}
```

Step 5: Handle file discovery order

go.sum may appear before or after go.mod in the `detectFiles` list. The approach above handles this by:

  1. Buffering ALL go.sum files first (they are skipped in the main loop)
  2. Processing go.mod normally through `AnalyzeLibrary`
  3. After the loop, merging buffered go.sum into matching go.mod results

This requires the dispatch to recognize go.sum (currently returns `parserNone`). Change `dispatch.go` to still return `parserNone` for go.sum, but the `scanLibraries` loop needs to check for go.sum BEFORE calling `detectParserType`.

Reference code locations

  • Trivy `lessThanGo117`: `~/go/pkg/mod/github.com/aquasecurity/trivy@v0.69.2/pkg/fanal/analyzer/language/golang/mod/mod.go` line 268
  • Trivy `mergeGoSum`: same file, line 279
  • Trivy `sum.NewParser`: `~/go/pkg/mod/github.com/aquasecurity/trivy@v0.69.2/pkg/dependency/parser/golang/sum/parse.go`
  • Vuls `scanLibraries`: `scanner/base.go` (after deps: remove fanal framework, call Trivy parsers directly #2476 merge)
  • Vuls `dispatch.go`: go.sum case at line ~98
  • Vuls `models/library.go`: `Library` struct (may need `Indirect` field)

Testing

  1. Create a Go 1.16 style go.mod fixture (no `// indirect` comments) + matching go.sum
  2. Verify: with fix, indirect deps from go.sum appear in scan results
  3. Verify: Go 1.17+ go.mod is unaffected (no merge needed)
  4. Add golden test fixtures for both cases
  5. Run `make compare-lockfile` to verify no regressions

Blocked by

Discovered during

PR #2476 review — confirmed via `make compare-lockfile` A/B comparison that master has the same behavior.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions