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: add git submodule analyzer #3345

Closed
wants to merge 1 commit into from
Closed
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 @@ -88,6 +88,7 @@ require (
github.com/testcontainers/testcontainers-go v0.17.0
github.com/tetratelabs/wazero v1.0.0
github.com/twitchtv/twirp v8.1.2+incompatible
github.com/whilp/git-urls v1.0.0
github.com/xlab/treeprint v1.1.0
go.etcd.io/bbolt v1.3.7
go.uber.org/zap v1.24.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,8 @@ github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU=
github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
Expand Down
4 changes: 2 additions & 2 deletions pkg/detector/library/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ func NewDriver(libType string) (Driver, error) {
// Only semver can be used for version ranges
// https://docs.conan.io/en/latest/versioning/version_ranges.html
comparer = compare.GenericComparer{}
case ftypes.Cocoapods:
log.Logger.Warn("CocoaPods is supported for SBOM, not for vulnerability scanning")
case ftypes.Cocoapods, ftypes.GitSubmodule:
log.Logger.Warnf("%s is supported for SBOM, not for vulnerability scanning", libType)
return Driver{}, ErrSBOMSupportOnly
case ftypes.CondaPkg:
log.Logger.Warn("Conda package is supported for SBOM, not for vulnerability scanning")
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/analyzer/all/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/buildinfo"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/executable"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/git/submodule"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/apk"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/dockerfile"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/secret"
Expand Down
3 changes: 2 additions & 1 deletion pkg/fanal/analyzer/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ const (
// ============
// Non-packaged
// ============
TypeExecutable Type = "executable"
TypeExecutable Type = "executable"
TypeGitSubmodule Type = "git-submodule"

// ============
// Image Config
Expand Down
108 changes: 108 additions & 0 deletions pkg/fanal/analyzer/git/submodule/submodule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package submodule

import (
"context"
"fmt"
"net/url"
"os"
"strings"

godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
"github.com/aquasecurity/trivy/pkg/fanal/types"

"github.com/go-git/go-git/v5"
giturls "github.com/whilp/git-urls"
"golang.org/x/xerrors"
)

func init() {
analyzer.RegisterAnalyzer(&gitSubmoduleAnalyzer{})
}

const version = 1

type gitSubmoduleAnalyzer struct{}

func (a gitSubmoduleAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
libs, deps, err := parseGitmodules(input.Dir)
if err != nil {
return nil, xerrors.Errorf("git repo parse error: %w", err)
}

return language.ToAnalysisResult(types.GitSubmodule, input.FilePath, "", libs, deps), nil
}

func (a gitSubmoduleAnalyzer) Required(_ string, fileInfo os.FileInfo) bool {
return fileInfo.Name() == types.GitModules
}

func (a gitSubmoduleAnalyzer) Type() analyzer.Type {
return analyzer.TypeGitSubmodule
}

func (a gitSubmoduleAnalyzer) Version() int {
return version
}

func parseGitmodules(inputDir string) ([]godeptypes.Library, []godeptypes.Dependency, error) {
repo, err := git.PlainOpen(inputDir)
if err != nil {
return nil, nil, err
}

w, err := repo.Worktree()
if err != nil {
return nil, nil, err
}

submodules, err := w.Submodules()
if err != nil {
return nil, nil, err
}

libs, _ := parseSubmodules(repo, &submodules)
return libs, nil, nil
}

func parseSubmodules(repo *git.Repository, submodules *git.Submodules) ([]godeptypes.Library, []godeptypes.Dependency) {
var libs []godeptypes.Library
var name *url.URL

for _, submodule := range *submodules {
remote := submodule.Config().URL

if strings.HasPrefix(remote, "../") {
// resolve relative URLs via root remote
rootRemote, err := getRemoteUrl(repo)
if err != nil {
return nil, nil
}

baseUrl, _ := giturls.Parse(fmt.Sprintf("%s/", rootRemote))
name, _ = baseUrl.Parse(remote)
} else {
name, _ = giturls.Parse(remote)
}

status, _ := submodule.Status()
version := status.Expected.String()

libs = append(libs, godeptypes.Library{
Name: name.String(),
Version: version,
})
}

return libs, nil
}

func getRemoteUrl(repo *git.Repository) (string, error) {
remote, err := repo.Remote("origin")
if err != nil {
return "", err
}

return remote.Config().URLs[0], nil
}
162 changes: 162 additions & 0 deletions pkg/fanal/analyzer/git/submodule/submodule_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package submodule

import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/utils"
)

func Test_gitSubmoduleAnalyzer_Analyze(t *testing.T) {
tests := []struct {
name string
filePath string
want *analyzer.AnalysisResult
}{
{
name: "https-url",
filePath: "testdata/https-url.gitmodules",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.GitSubmodule,
FilePath: types.GitModules,
Libraries: []types.Package{
{
Name: "https://github.com/org/repository.git",
Version: "ca82a6dff817ec66f44342007202690a93763949",
},
},
},
},
},
},
{
name: "git-url",
filePath: "testdata/git-url.gitmodules",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.GitSubmodule,
FilePath: types.GitModules,
Libraries: []types.Package{
{
Name: "ssh://[email protected]/org/repository.git",
Version: "ca82a6dff817ec66f44342007202690a93763949",
},
},
},
},
},
},
{
name: "ssh-url",
filePath: "testdata/ssh-url.gitmodules",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.GitSubmodule,
FilePath: types.GitModules,
Libraries: []types.Package{
{
Name: "ssh://[email protected]/org/repository.git",
Version: "ca82a6dff817ec66f44342007202690a93763949",
},
},
},
},
},
},
{
name: "relative-url",
filePath: "testdata/relative-url.gitmodules",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.GitSubmodule,
FilePath: types.GitModules,
Libraries: []types.Package{
{
Name: "https://github.com/org/repository.git",
Version: "ca82a6dff817ec66f44342007202690a93763949",
},
},
},
},
},
},
{
name: "missing-submodule",
filePath: "testdata/missing-submodule.gitmodules",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
currentDir, err := os.Getwd()
require.NoError(t, err)

dir := t.TempDir()
destFilePath := filepath.Join(dir, types.GitModules)

_, err = utils.CopyFile(tt.filePath, destFilePath)
require.NoError(t, err)

err = initRepoWithSubmodules(dir)
require.NoError(t, err)

err = os.Chdir(dir)
require.NoError(t, err)
defer os.Chdir(currentDir)

a := gitSubmoduleAnalyzer{}
got, err := a.Analyze(context.Background(), analyzer.AnalysisInput{
Dir: dir,
FilePath: types.GitModules,
})
assert.Equal(t, tt.want, got)
})
}
}

func initRepoWithSubmodules(dir string) error {
repo, err := git.PlainInit(dir, false)
if err != nil {
return err
}

_, err = repo.CreateRemote(&config.RemoteConfig{
Name: "origin",
URLs: []string{"https://github.com/org/repository.git"},
})
if err != nil {
return err
}

updateIndexCmd := exec.Command(
"git",
"update-index",
"--add",
"--cacheinfo",
"160000",
"ca82a6dff817ec66f44342007202690a93763949",
"submodule",
)
updateIndexCmd.Dir = dir
updateIndexCmd.Run()
if err != nil {
return err
}

return nil
}
11 changes: 11 additions & 0 deletions pkg/fanal/analyzer/git/submodule/testdata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Git submodule testdata

The examples in this testdata directory test a few common Git URL formats. For a full
list of supported Git URL formats, see:
https://stackoverflow.com/questions/31801271/what-are-the-supported-git-url-formats

For the git plumbing commands involved in the faked remote submodule test setup, see
https://stackoverflow.com/questions/34562333/is-there-a-way-to-git-submodule-add-a-repo-without-cloning-it.

Files here are not valid Git submodule configuration filenames. They are copied in each test case
to `t.TempDir` as `.gitmodules`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "submodule"]
path = submodule
url = [email protected]:org/repository.git
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "submodule"]
path = submodule
url = https://github.com/org/repository.git
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This repository includes a valid entry in .git/index but no .gitmodules entry
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "submodule"]
path = submodule
url = ../repository.git
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "submodule"]
path = submodule
url = ssh://[email protected]/org/repository.git
6 changes: 6 additions & 0 deletions pkg/fanal/types/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const (
Pub = "pub"
Hex = "hex"

// Non-packaged dependencies
GitSubmodule = "git-submodule"

// Config files
YAML = "yaml"
JSON = "json"
Expand Down Expand Up @@ -83,4 +86,7 @@ const (
PubSpecLock = "pubspec.lock"

MixLock = "mix.lock"

// Non-packaged file names
GitModules = ".gitmodules"
)