From 30c9f90bf809b67a257b39b9d68addcb40d028b9 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Sun, 17 Jul 2022 16:24:28 +0530 Subject: [PATCH] feat(repo): add support for branch, commit, & tag (#2494) Co-authored-by: knqyf263 --- docs/docs/references/cli/repo.md | 5 + .../references/customization/config-file.md | 18 ++++ .../vulnerability/scanning/git-repository.md | 24 +++++ pkg/commands/app.go | 1 + pkg/commands/artifact/run.go | 3 + pkg/fanal/artifact/artifact.go | 3 + pkg/fanal/artifact/remote/git.go | 34 ++++++- pkg/fanal/artifact/remote/git_test.go | 88 ++++++++++++++++-- pkg/fanal/artifact/remote/testdata/test | 1 + .../0d/8bf3f07c3970b3e38c2cfb1c619cb86fae76d2 | 2 + .../1c/1d7deed649fbecd66fab423ccd9d001bf9ff91 | Bin 0 -> 59 bytes .../27/aaec53f92314d9438a53c703f169d2cbf5001a | Bin 0 -> 86 bytes .../6a/c152fe2b87cb5e243414df71790a32912e778d | 3 + .../c0/42cd14d2b999cade090785af47e9f8b8e342ff | Bin 0 -> 34 bytes .../d7/937c5f0ce7f2054e4e3be65ab3cd0f9462dc1b | Bin 0 -> 165 bytes .../e2/4866d1d31ddffdb27fbcf583d5deb4386d5145 | Bin 0 -> 53 bytes .../f4/836be6497e83e13dc0cfbce7e6b973b1ea511d | Bin 0 -> 40 bytes .../testdata/test.git/refs/heads/master | 2 +- .../testdata/test.git/refs/heads/valid-branch | 1 + .../remote/testdata/test.git/refs/tags/v1.0.0 | 1 + pkg/flag/options.go | 9 ++ pkg/flag/repo.go | 58 ++++++++++++ 22 files changed, 242 insertions(+), 11 deletions(-) create mode 160000 pkg/fanal/artifact/remote/testdata/test create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/0d/8bf3f07c3970b3e38c2cfb1c619cb86fae76d2 create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/1c/1d7deed649fbecd66fab423ccd9d001bf9ff91 create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/27/aaec53f92314d9438a53c703f169d2cbf5001a create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/6a/c152fe2b87cb5e243414df71790a32912e778d create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/c0/42cd14d2b999cade090785af47e9f8b8e342ff create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/d7/937c5f0ce7f2054e4e3be65ab3cd0f9462dc1b create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/e2/4866d1d31ddffdb27fbcf583d5deb4386d5145 create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/objects/f4/836be6497e83e13dc0cfbce7e6b973b1ea511d create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/refs/heads/valid-branch create mode 100644 pkg/fanal/artifact/remote/testdata/test.git/refs/tags/v1.0.0 create mode 100644 pkg/flag/repo.go 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 0000000000000000000000000000000000000000..1c1c8f2a4f50c6b83a6645b026b6f2c722d2e748 GIT binary patch literal 59 zcmb8#W%j<>n*Fxv)5_&?<#!Z>Hon+ P#njD0l#xMWkMec^NU0R} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1e1970b9b3e25d56678d5ce984f5ed0d98cac0f3 GIT binary patch literal 86 zcmV-c0IC0Y0V^p=O;s?rWH2-^Ff%bxNX*MG$w)0KNi8nXE2$`9_|lyH%(Jffq3wb5 sd!9erS-kO8pe$4=T+IQevm%#v&OCLGlf8Al`^z6Y9y|R90QS@-;8*}CxBvhE literal 0 HcmV?d00001 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.@&*&˔Oz5XYFY56%;DVqHdGKZmU!usm?8;|C|y*LC^r?fxn_PA8hnP1|^e&VYlUkLK)++U@?d T$v+K+%op8cZBBgweIiasKjBa- literal 0 HcmV?d00001 diff --git a/pkg/fanal/artifact/remote/testdata/test.git/objects/e2/4866d1d31ddffdb27fbcf583d5deb4386d5145 b/pkg/fanal/artifact/remote/testdata/test.git/objects/e2/4866d1d31ddffdb27fbcf583d5deb4386d5145 new file mode 100644 index 0000000000000000000000000000000000000000..5f1213f2ea4c4104e96cda98384a0f3eb001a3c7 GIT binary patch literal 53 zcmV-50LuS(0V^p=O;s>9V=y!@Ff%bxC`m0Y(JQGaVL0G)R^-yonWye?vbU~xfB9p_ LW2gTBQz8=1`p*`L literal 0 HcmV?d00001 diff --git a/pkg/fanal/artifact/remote/testdata/test.git/objects/f4/836be6497e83e13dc0cfbce7e6b973b1ea511d b/pkg/fanal/artifact/remote/testdata/test.git/objects/f4/836be6497e83e13dc0cfbce7e6b973b1ea511d new file mode 100644 index 0000000000000000000000000000000000000000..bec9b64a3358c80dfa2c559736048d0fe1f5fbae GIT binary patch literal 40 ycmV+@0N4L`0ZYosPf{>4V<^eUELH%b#Jv2HjMO59lGKV4g|y6^R6PL4{0qQ%+7ogB literal 0 HcmV?d00001 diff --git a/pkg/fanal/artifact/remote/testdata/test.git/refs/heads/master b/pkg/fanal/artifact/remote/testdata/test.git/refs/heads/master index bff68b6968..21969947cb 100644 --- a/pkg/fanal/artifact/remote/testdata/test.git/refs/heads/master +++ b/pkg/fanal/artifact/remote/testdata/test.git/refs/heads/master @@ -1 +1 @@ -c906fc4a94762f8a2c77c718947143d16e4e9ec7 +0d8bf3f07c3970b3e38c2cfb1c619cb86fae76d2 diff --git a/pkg/fanal/artifact/remote/testdata/test.git/refs/heads/valid-branch b/pkg/fanal/artifact/remote/testdata/test.git/refs/heads/valid-branch new file mode 100644 index 0000000000..22301d650c --- /dev/null +++ b/pkg/fanal/artifact/remote/testdata/test.git/refs/heads/valid-branch @@ -0,0 +1 @@ +d7937c5f0ce7f2054e4e3be65ab3cd0f9462dc1b diff --git a/pkg/fanal/artifact/remote/testdata/test.git/refs/tags/v1.0.0 b/pkg/fanal/artifact/remote/testdata/test.git/refs/tags/v1.0.0 new file mode 100644 index 0000000000..bff68b6968 --- /dev/null +++ b/pkg/fanal/artifact/remote/testdata/test.git/refs/tags/v1.0.0 @@ -0,0 +1 @@ +c906fc4a94762f8a2c77c718947143d16e4e9ec7 diff --git a/pkg/flag/options.go b/pkg/flag/options.go index 19f45e8006..2a3becb63d 100644 --- a/pkg/flag/options.go +++ b/pkg/flag/options.go @@ -51,6 +51,7 @@ type Flags struct { LicenseFlagGroup *LicenseFlagGroup MisconfFlagGroup *MisconfFlagGroup RemoteFlagGroup *RemoteFlagGroup + RepoFlagGroup *RepoFlagGroup ReportFlagGroup *ReportFlagGroup SBOMFlagGroup *SBOMFlagGroup ScanFlagGroup *ScanFlagGroup @@ -68,6 +69,7 @@ type Options struct { LicenseOptions MisconfOptions RemoteOptions + RepoOptions ReportOptions SBOMOptions ScanOptions @@ -209,6 +211,9 @@ func (f *Flags) groups() []FlagGroup { if f.RemoteFlagGroup != nil { groups = append(groups, f.RemoteFlagGroup) } + if f.RepoFlagGroup != nil { + groups = append(groups, f.RepoFlagGroup) + } return groups } @@ -302,6 +307,10 @@ func (f *Flags) ToOptions(appVersion string, args []string, globalFlags *GlobalF opts.RemoteOptions = f.RemoteFlagGroup.ToOptions() } + if f.RepoFlagGroup != nil { + opts.RepoOptions = f.RepoFlagGroup.ToOptions() + } + if f.ReportFlagGroup != nil { opts.ReportOptions, err = f.ReportFlagGroup.ToOptions(output) if err != nil { diff --git a/pkg/flag/repo.go b/pkg/flag/repo.go new file mode 100644 index 0000000000..dfbffa8e69 --- /dev/null +++ b/pkg/flag/repo.go @@ -0,0 +1,58 @@ +package flag + +var ( + FetchBranchFlag = Flag{ + Name: "branch", + ConfigName: "repository.branch", + Value: "", + Usage: "pass the branch name to be scanned", + } + FetchCommitFlag = Flag{ + Name: "commit", + ConfigName: "repository.commit", + Value: "", + Usage: "pass the commit hash to be scanned", + } + FetchTagFlag = Flag{ + Name: "tag", + ConfigName: "repository.tag", + Value: "", + Usage: "pass the tag name to be scanned", + } +) + +type RepoFlagGroup struct { + Branch *Flag + Commit *Flag + Tag *Flag +} + +type RepoOptions struct { + RepoBranch string + RepoCommit string + RepoTag string +} + +func NewRepoFlagGroup() *RepoFlagGroup { + return &RepoFlagGroup{ + Branch: &FetchBranchFlag, + Commit: &FetchCommitFlag, + Tag: &FetchTagFlag, + } +} + +func (f *RepoFlagGroup) Name() string { + return "Repository" +} + +func (f *RepoFlagGroup) Flags() []*Flag { + return []*Flag{f.Branch, f.Commit, f.Tag} +} + +func (f *RepoFlagGroup) ToOptions() RepoOptions { + return RepoOptions{ + RepoBranch: getString(f.Branch), + RepoCommit: getString(f.Commit), + RepoTag: getString(f.Tag), + } +}