diff --git a/docs/docs/references/cli/repo.md b/docs/docs/references/cli/repo.md index 8fa81e73b3..682b524df5 100644 --- a/docs/docs/references/cli/repo.md +++ b/docs/docs/references/cli/repo.md @@ -65,6 +65,11 @@ Client/Server Flags --server string server address in client mode --token string for authentication in client/server mode --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + +Repository Flags + --branch string pass the branch name to be scanned + --commit string pass the commit hash to be scanned + --tag string pass the tag name to be scanned Global Flags: --cache-dir string cache directory (default "/Users/teppei/Library/Caches/trivy") diff --git a/docs/docs/references/customization/config-file.md b/docs/docs/references/customization/config-file.md index 079780e495..fc6766b636 100644 --- a/docs/docs/references/customization/config-file.md +++ b/docs/docs/references/customization/config-file.md @@ -240,6 +240,24 @@ kubernetes: namespace: ``` +## Repository Options +Available with git repository scanning (`trivy repo`) + +``` +repository: + # Same as '--branch' + # Default is empty + branch: + + # Same as '--commit' + # Default is empty + commit: + + # Same as '--tag' + # Default is empty + tag: +``` + ## Client/Server Options Available in client/server mode diff --git a/docs/docs/vulnerability/scanning/git-repository.md b/docs/docs/vulnerability/scanning/git-repository.md index 496ad0c8b5..1c86585a0e 100644 --- a/docs/docs/vulnerability/scanning/git-repository.md +++ b/docs/docs/vulnerability/scanning/git-repository.md @@ -147,6 +147,30 @@ Total: 20 (UNKNOWN: 3, LOW: 0, MEDIUM: 7, HIGH: 5, CRITICAL: 5) +## Scanning a Branch + +Pass a `--branch` agrument with a valid branch name on the remote repository provided: + +``` +$ trivy repo --branch +``` + +## Scanning upto a Commit + +Pass a `--commit` agrument with a valid commit hash on the remote repository provided: + +``` +$ trivy repo --commit +``` + +## Scanning a Tag + +Pass a `--tag` agrument with a valid tag on the remote repository provided: + +``` +$ trivy repo --tag +``` + ## Scanning Private Repositories In order to scan private GitHub or GitLab repositories, the environment variable `GITHUB_TOKEN` or `GITLAB_TOKEN` must be set, respectively, with a valid token that has access to the private repository being scanned. diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 00f39e0077..33a251eee0 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -400,6 +400,7 @@ func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { ScanFlagGroup: flag.NewScanFlagGroup(), SecretFlagGroup: flag.NewSecretFlagGroup(), VulnerabilityFlagGroup: flag.NewVulnerabilityFlagGroup(), + RepoFlagGroup: flag.NewRepoFlagGroup(), } cmd := &cobra.Command{ diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go index 6bb1087583..e490ee34dc 100644 --- a/pkg/commands/artifact/run.go +++ b/pkg/commands/artifact/run.go @@ -497,6 +497,9 @@ func initScannerConfig(opts flag.Options, cacheClient cache.Cache) (ScannerConfi InsecureSkipTLS: opts.Insecure, Offline: opts.OfflineScan, NoProgress: opts.NoProgress || opts.Quiet, + RepoBranch: opts.RepoBranch, + RepoCommit: opts.RepoCommit, + RepoTag: opts.RepoTag, // For misconfiguration scanning MisconfScannerOption: configScannerOptions, diff --git a/pkg/fanal/artifact/artifact.go b/pkg/fanal/artifact/artifact.go index e8fdf442fd..530b1ba310 100644 --- a/pkg/fanal/artifact/artifact.go +++ b/pkg/fanal/artifact/artifact.go @@ -20,6 +20,9 @@ type Option struct { Offline bool InsecureSkipTLS bool AppDirs []string + RepoBranch string + RepoCommit string + RepoTag string MisconfScannerOption misconf.ScannerOption SecretScannerOption secret.ScannerOption diff --git a/pkg/fanal/artifact/remote/git.go b/pkg/fanal/artifact/remote/git.go index eabbd8619d..b25eb23d17 100644 --- a/pkg/fanal/artifact/remote/git.go +++ b/pkg/fanal/artifact/remote/git.go @@ -7,6 +7,7 @@ import ( "os" git "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport/http" "golang.org/x/xerrors" @@ -39,7 +40,6 @@ func NewArtifact(rawurl string, c cache.ArtifactCache, artifactOpt artifact.Opti URL: u.String(), Auth: gitAuth(), Progress: os.Stdout, - Depth: 1, InsecureSkipTLS: artifactOpt.InsecureSkipTLS, } @@ -48,9 +48,37 @@ func NewArtifact(rawurl string, c cache.ArtifactCache, artifactOpt artifact.Opti cloneOptions.Progress = nil } - _, err = git.PlainClone(tmpDir, false, &cloneOptions) + if artifactOpt.RepoCommit == "" { + cloneOptions.Depth = 1 + } + + if artifactOpt.RepoBranch != "" { + cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(artifactOpt.RepoBranch) + cloneOptions.SingleBranch = true + } + + if artifactOpt.RepoTag != "" { + cloneOptions.ReferenceName = plumbing.NewTagReferenceName(artifactOpt.RepoTag) + cloneOptions.SingleBranch = true + } + + r, err := git.PlainClone(tmpDir, false, &cloneOptions) if err != nil { - return nil, cleanup, xerrors.Errorf("git error: %w", err) + return nil, cleanup, xerrors.Errorf("git clone error: %w", err) + } + + if artifactOpt.RepoCommit != "" { + w, err := r.Worktree() + if err != nil { + return nil, cleanup, xerrors.Errorf("git worktree error: %w", err) + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(artifactOpt.RepoCommit), + }) + if err != nil { + return nil, cleanup, xerrors.Errorf("git checkout error: %w", err) + } } cleanup = func() { diff --git a/pkg/fanal/artifact/remote/git_test.go b/pkg/fanal/artifact/remote/git_test.go index 6580d5f204..b2a6a5a16a 100644 --- a/pkg/fanal/artifact/remote/git_test.go +++ b/pkg/fanal/artifact/remote/git_test.go @@ -38,11 +38,14 @@ func TestNewArtifact(t *testing.T) { rawurl string c cache.ArtifactCache noProgress bool + repoBranch string + repoTag string + repoCommit string } tests := []struct { - name string - args args - wantErr bool + name string + args args + assertion assert.ErrorAssertionFunc }{ { name: "happy path", @@ -51,6 +54,7 @@ func TestNewArtifact(t *testing.T) { c: nil, noProgress: false, }, + assertion: assert.NoError, }, { name: "happy noProgress", @@ -59,6 +63,34 @@ func TestNewArtifact(t *testing.T) { c: nil, noProgress: true, }, + assertion: assert.NoError, + }, + { + name: "branch", + args: args{ + rawurl: ts.URL + "/test.git", + c: nil, + repoBranch: "valid-branch", + }, + assertion: assert.NoError, + }, + { + name: "tag", + args: args{ + rawurl: ts.URL + "/test.git", + c: nil, + repoTag: "v1.0.0", + }, + assertion: assert.NoError, + }, + { + name: "commit", + args: args{ + rawurl: ts.URL + "/test.git", + c: nil, + repoCommit: "6ac152fe2b87cb5e243414df71790a32912e778d", + }, + assertion: assert.NoError, }, { name: "sad path", @@ -67,7 +99,9 @@ func TestNewArtifact(t *testing.T) { c: nil, noProgress: false, }, - wantErr: true, + assertion: func(t assert.TestingT, err error, args ...interface{}) bool { + return assert.ErrorContains(t, err, "repository not found") + }, }, { name: "invalid url", @@ -76,14 +110,54 @@ func TestNewArtifact(t *testing.T) { c: nil, noProgress: false, }, - wantErr: true, + assertion: func(t assert.TestingT, err error, args ...interface{}) bool { + return assert.ErrorContains(t, err, "url parse error") + }, + }, + { + name: "invalid branch", + args: args{ + rawurl: ts.URL + "/test.git", + c: nil, + repoBranch: "invalid-branch", + }, + assertion: func(t assert.TestingT, err error, args ...interface{}) bool { + return assert.ErrorContains(t, err, `couldn't find remote ref "refs/heads/invalid-branch"`) + }, + }, + { + name: "invalid tag", + args: args{ + rawurl: ts.URL + "/test.git", + c: nil, + repoTag: "v1.0.9", + }, + assertion: func(t assert.TestingT, err error, args ...interface{}) bool { + return assert.ErrorContains(t, err, `couldn't find remote ref "refs/tags/v1.0.9"`) + }, + }, + { + name: "invalid commit", + args: args{ + rawurl: ts.URL + "/test.git", + c: nil, + repoCommit: "6ac152fe2b87cb5e243414df71790a32912e778e", + }, + assertion: func(t assert.TestingT, err error, args ...interface{}) bool { + return assert.ErrorContains(t, err, "git checkout error: object not found") + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, cleanup, err := NewArtifact(tt.args.rawurl, tt.args.c, artifact.Option{NoProgress: tt.args.noProgress}) - assert.Equal(t, tt.wantErr, err != nil) + _, cleanup, err := NewArtifact(tt.args.rawurl, tt.args.c, artifact.Option{ + NoProgress: tt.args.noProgress, + RepoBranch: tt.args.repoBranch, + RepoTag: tt.args.repoTag, + RepoCommit: tt.args.repoCommit, + }) + tt.assertion(t, err) defer cleanup() }) } diff --git a/pkg/fanal/artifact/remote/testdata/test b/pkg/fanal/artifact/remote/testdata/test new file mode 160000 index 0000000000..0d8bf3f07c --- /dev/null +++ b/pkg/fanal/artifact/remote/testdata/test @@ -0,0 +1 @@ +Subproject commit 0d8bf3f07c3970b3e38c2cfb1c619cb86fae76d2 diff --git a/pkg/fanal/artifact/remote/testdata/test.git/objects/0d/8bf3f07c3970b3e38c2cfb1c619cb86fae76d2 b/pkg/fanal/artifact/remote/testdata/test.git/objects/0d/8bf3f07c3970b3e38c2cfb1c619cb86fae76d2 new file mode 100644 index 0000000000..9baa2c164c --- /dev/null +++ b/pkg/fanal/artifact/remote/testdata/test.git/objects/0d/8bf3f07c3970b3e38c2cfb1c619cb86fae76d2 @@ -0,0 +1,2 @@ +xAj0s+X˲ `,"^cG Ї{MGxb$nZӄ3dVul|]!rvLy 'WQ>9/DjԺYϥrh߼1\~un[ \ 4[f'}:*? +mb~T \ No newline at end of file diff --git a/pkg/fanal/artifact/remote/testdata/test.git/objects/1c/1d7deed649fbecd66fab423ccd9d001bf9ff91 b/pkg/fanal/artifact/remote/testdata/test.git/objects/1c/1d7deed649fbecd66fab423ccd9d001bf9ff91 new file mode 100644 index 0000000000..1c1c8f2a4f Binary files /dev/null and b/pkg/fanal/artifact/remote/testdata/test.git/objects/1c/1d7deed649fbecd66fab423ccd9d001bf9ff91 differ diff --git a/pkg/fanal/artifact/remote/testdata/test.git/objects/27/aaec53f92314d9438a53c703f169d2cbf5001a b/pkg/fanal/artifact/remote/testdata/test.git/objects/27/aaec53f92314d9438a53c703f169d2cbf5001a new file mode 100644 index 0000000000..1e1970b9b3 Binary files /dev/null and b/pkg/fanal/artifact/remote/testdata/test.git/objects/27/aaec53f92314d9438a53c703f169d2cbf5001a differ diff --git a/pkg/fanal/artifact/remote/testdata/test.git/objects/6a/c152fe2b87cb5e243414df71790a32912e778d b/pkg/fanal/artifact/remote/testdata/test.git/objects/6a/c152fe2b87cb5e243414df71790a32912e778d new file mode 100644 index 0000000000..f40c82e685 --- /dev/null +++ b/pkg/fanal/artifact/remote/testdata/test.git/objects/6a/c152fe2b87cb5e243414df71790a32912e778d @@ -0,0 +1,3 @@ +xK0DgS>Br5Xqp$mr@-Qym +~dc6.@&*&˔O