Skip to content

feat(license): scan vendor directory for license for go.mod files #8689

New issue

Have a question about this project? No Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “No 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? No Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

oneum20
Copy link

@oneum20 oneum20 commented Apr 5, 2025

Description

This PR adds support for scanning the vendor directory when detecting licenses for Go modules. Currently, Trivy only checks for licenses in $GOPATH/pkg/mod, but when users use go mod vendor command, the dependencies are stored in the vendor directory without their own go.mod files.

Related issues

Checklist

  • I've read the guidelines for contributing to this repository.
  • I've followed the conventions in the PR title.
  • I've added tests that prove my fix is effective or that my feature works.
  • I've updated the documentation with the relevant information (if needed).
  • I've added usage information (if the PR introduces new options)
  • I've included a "before" and "after" example to the description (if the PR is a user interface change).

Copy link
Contributor

@DmitriyLewen DmitriyLewen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @oneum20
Thanks for your work!

Left a few comments. Take a look, when you have time, please
Also we need to update docs (see https://trivy.dev/latest/docs/coverage/language/golang/#license)

@@ -143,6 +149,14 @@ func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application) error {
usedPkgs := lo.SliceToMap(app.Packages, func(pkg types.Package) (string, types.Package) {
return pkg.Name, pkg
})

// Check if the vendor directory exists
vendorPath := filepath.Join(absPath, filepath.Dir(app.FilePath), "vendor")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if $GOPATH/pkg/mod doesn't exist - you will not check vendor dir.

if !fsutils.DirExists(modPath) {
a.logger.Debug("GOPATH not found. Need 'go mod download' to fill licenses and dependency relationships",
log.String("GOPATH", modPath))
return nil
}

@@ -123,7 +124,12 @@ func (a *gomodAnalyzer) Version() int {
}

// fillAdditionalData collects licenses and dependency relationships, then update applications.
func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application) error {
func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application, fsys fs.FS) error {
absPath, err := filepath.Abs(fsys.(*mapfs.FS).GetUnderlyingRoot())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If repository contains required files - we try to add them into CompositeFS.
e.g. node_modules dir for npm and yarn.
So i suggest to add vendor dir into CompositeFS (use Required function)

Comment on lines 168 to 172
modDirSuffix := fmt.Sprintf("@%s", lib.Version)
if modPath == vendorPath {
modDirSuffix = ""
}
modDir := filepath.Join(modPath, fmt.Sprintf("%s%s", normalizeModName(lib.Name), modDirSuffix))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be easier to understand this way.

Suggested change
modDirSuffix := fmt.Sprintf("@%s", lib.Version)
if modPath == vendorPath {
modDirSuffix = ""
}
modDir := filepath.Join(modPath, fmt.Sprintf("%s%s", normalizeModName(lib.Name), modDirSuffix))
// Package dir from `vendor` dir doesn't have version suffix.
modDirName := normalizeModName(lib.Name)
if modPath != vendorPath {
// Add version suffix for packages from $$GOPATH
// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v1.0.0
modDirName = fmt.Sprintf("%s@%s", modDirName, lib.Version)
}
modDir := filepath.Join(modPath, modDirName)

@@ -151,7 +165,11 @@ func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application) error {
}

// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v1.0.0
modDir := filepath.Join(modPath, fmt.Sprintf("%s@%s", normalizeModName(lib.Name), lib.Version))
modDirSuffix := fmt.Sprintf("@%s", lib.Version)
if modPath == vendorPath {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can add verndorDirFound bool variable for that.

@@ -165,7 +183,8 @@ func (a *gomodAnalyzer) fillAdditionalData(apps []types.Application) error {
}

// Collect dependencies of the direct dependency
if dep, err := a.collectDeps(modDir, lib.ID); err != nil {
gopathModDir := filepath.Join(gopath, "pkg", "mod", fmt.Sprintf("%s@%s", normalizeModName(lib.Name), lib.Version))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add comment why we use another path here.

@oneum20
Copy link
Author

oneum20 commented Apr 10, 2025

Hi @DmitriyLewen!

Thanks for your review!
I've addressed the feedback and updated the code accordingly.
Could you please take another look when you have a moment?

Thanks!

@oneum20 oneum20 force-pushed the FEAT-8527-scan-vendor-license branch from 07e4bfe to 467e9ed Compare April 12, 2025 01:35
Copy link
Contributor

@DmitriyLewen DmitriyLewen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, left small comments

@@ -111,7 +112,16 @@ func (a *gomodAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalys

func (a *gomodAnalyzer) Required(filePath string, _ os.FileInfo) bool {
fileName := filepath.Base(filePath)
return slices.Contains(requiredFiles, fileName)

if slices.Contains(requiredFiles, fileName) && !xpath.Contains(filePath, "vendor") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove vendor dir check here.
As we told in #8527 (comment) - vendor dir doesn't have go.mod/go.sum files.

But we can add comment about this case with link to your comment,


// Check if the vendor directory exists
_, err := fs.Stat(fsys, vendorDir)
vendorDirFound := err == nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use errors.Is(err, os.ErrNotExist) to check that dir exists

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and we need to check that this is dir (not file)

Comment on lines 199 to 200
// Collect dependencies of the direct dependency from $GOPATH/pkg/mod because the vendor directory doesn't have go.mod files.
gopathModDir := filepath.Join(gopath, "pkg", "mod", fmt.Sprintf("%s@%s", normalizeModName(lib.Name), lib.Version))
Copy link
Contributor

@DmitriyLewen DmitriyLewen Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add check here or in collectDeps func

Suggested change
// Collect dependencies of the direct dependency from $GOPATH/pkg/mod because the vendor directory doesn't have go.mod files.
gopathModDir := filepath.Join(gopath, "pkg", "mod", fmt.Sprintf("%s@%s", normalizeModName(lib.Name), lib.Version))
if !gopathModDirFound {
continue
}
// Collect dependencies of the direct dependency from $GOPATH/pkg/mod because the vendor directory doesn't have go.mod files.
gopathModDir := filepath.Join(gopath, "pkg", "mod", fmt.Sprintf("%s@%s", normalizeModName(lib.Name), lib.Version))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review. :)
I've addressed the other points and looked into this suggestion.

It seems the check is already implemented here:

func (a *gomodAnalyzer) collectDeps(modDir, pkgID string) (types.Dependency, error) {
// e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/go.mod
modPath := filepath.Join(modDir, "go.mod")
f, err := os.Open(modPath)
if errors.Is(err, fs.ErrNotExist) {
a.logger.Debug("Unable to identify dependencies as it doesn't support Go modules",
log.String("module", pkgID))
return types.Dependency{}, nil
} else if err != nil {
return types.Dependency{}, xerrors.Errorf("file open error: %w", err)
}

Was there perhaps a different intention I might have missed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are 2 point:

  1. We try to use "early return" logic.
  2. We will check go.mod file for each dependency (but you know that GOPATH dir doesn't exist), so this is extra resources.

@DmitriyLewen
Copy link
Contributor

@oneum20 I refactored a bit, can you take a look and confirm that i didn't break your logic? 😄

@oneum20
Copy link
Author

oneum20 commented Apr 16, 2025

@DmitriyLewen
Thanks for the refactor!
Everything looks fine on my end. 😃

Copy link
Contributor

@DmitriyLewen DmitriyLewen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@knqyf263 can you also take a look, please?

No Sign up for free to join this conversation on GitHub. Already have an account? No Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants