diff --git a/pkg/giturl/giturl.go b/pkg/giturl/giturl.go index facd3ec7f..0fdc200e6 100644 --- a/pkg/giturl/giturl.go +++ b/pkg/giturl/giturl.go @@ -2,6 +2,7 @@ package giturl import ( "net/url" + "strconv" "strings" "github.com/pkg/errors" @@ -13,8 +14,29 @@ const ( providerGithub provider = "Github" providerGitlab provider = "Gitlab" providerBitbucket provider = "Bitbucket" + providerAzure provider = "Azure" + + urlGithub = "github.com/" + urlGitlab = "gitlab.com/" + urlBitbucket = "bitbucket.org/" + urlAzure = "dev.azure.com/" ) +func determineProvider(repo string) provider { + switch { + case strings.Contains(repo, urlGithub): + return providerGithub + case strings.Contains(repo, urlGitlab): + return providerGitlab + case strings.Contains(repo, urlBitbucket): + return providerBitbucket + case strings.Contains(repo, urlAzure): + return providerAzure + default: + return "" + } +} + func NormalizeBitbucketRepo(repoURL string) (string, error) { if !strings.HasPrefix(repoURL, "https") { return "", errors.New("Bitbucket requires https repo urls: e.g. https://bitbucket.org/org/repo.git") @@ -88,3 +110,35 @@ func NormalizeOrgRepoURL(provider provider, repoURL string) (string, error) { parsed.Path += ".git" return parsed.String(), nil } + +// GenerateLink crafts a link to the specific file from a commit. +// Supports GitHub, GitLab, Bitbucket, and Azure Repos. +// If the provider supports hyperlinks to specific lines, the line number will be included. +func GenerateLink(repo, commit, file string, line int64) string { + switch determineProvider(repo) { + case providerBitbucket: + return repo[:len(repo)-4] + "/commits/" + commit + + case providerGithub, providerGitlab: + var baseLink string + if file == "" { + baseLink = repo[:len(repo)-4] + "/commit/" + commit + } else { + baseLink = repo[:len(repo)-4] + "/blob/" + commit + "/" + file + if line > 0 { + baseLink += "#L" + strconv.FormatInt(line, 10) + } + } + return baseLink + + case providerAzure: + baseLink := repo + "?path=" + file + "&version=GB" + commit + if line > 0 { + baseLink += "&line=" + strconv.FormatInt(line, 10) + } + return baseLink + + default: + return "" + } +} diff --git a/pkg/giturl/giturl_test.go b/pkg/giturl/giturl_test.go index 1d8a450a1..69c0556ce 100644 --- a/pkg/giturl/giturl_test.go +++ b/pkg/giturl/giturl_test.go @@ -7,6 +7,8 @@ import ( ) func Test_NormalizeOrgRepoURL(t *testing.T) { + t.Parallel() + tests := map[string]struct { Provider provider Repo string @@ -43,6 +45,8 @@ func Test_NormalizeOrgRepoURL(t *testing.T) { } func Test_NormalizeBitbucketRepo(t *testing.T) { + t.Parallel() + tests := map[string]struct { Repo string Out string @@ -69,6 +73,8 @@ func Test_NormalizeBitbucketRepo(t *testing.T) { } func Test_NormalizeGitlabRepo(t *testing.T) { + t.Parallel() + tests := map[string]struct { Repo string Out string @@ -93,3 +99,73 @@ func Test_NormalizeGitlabRepo(t *testing.T) { } } } + +func TestGenerateLink(t *testing.T) { + t.Parallel() + + type args struct { + repo string + commit string + file string + line int64 + } + tests := []struct { + name string + args args + want string + }{ + { + name: "github link gen", + args: args{ + repo: "https://github.com/trufflesec-julian/confluence-go-api.git", + commit: "047b4a2ba42fc5b6c0bd535c5307434a666db5ec", + file: ".gitignore", + }, + want: "https://github.com/trufflesec-julian/confluence-go-api/blob/047b4a2ba42fc5b6c0bd535c5307434a666db5ec/.gitignore", + }, + { + name: "github link gen with line", + args: args{ + repo: "https://github.com/trufflesec-julian/confluence-go-api.git", + commit: "047b4a2ba42fc5b6c0bd535c5307434a666db5ec", + file: ".gitignore", + line: int64(4), + }, + want: "https://github.com/trufflesec-julian/confluence-go-api/blob/047b4a2ba42fc5b6c0bd535c5307434a666db5ec/.gitignore#L4", + }, + { + name: "github link gen - no file", + args: args{ + repo: "https://github.com/trufflesec-julian/confluence-go-api.git", + commit: "047b4a2ba42fc5b6c0bd535c5307434a666db5ec", + }, + want: "https://github.com/trufflesec-julian/confluence-go-api/commit/047b4a2ba42fc5b6c0bd535c5307434a666db5ec", + }, + { + name: "Azure link gen", + args: args{ + repo: "https://dev.azure.com/org/project/_git/repo", + commit: "abcdef", + file: "main.go", + }, + want: "https://dev.azure.com/org/project/_git/repo?path=main.go&version=GBabcdef", + }, + { + name: "Azure link gen with line", + args: args{ + repo: "https://dev.azure.com/org/project/_git/repo", + commit: "abcdef", + file: "main.go", + line: int64(20), + }, + want: "https://dev.azure.com/org/project/_git/repo?path=main.go&version=GBabcdef&line=20", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GenerateLink(tt.args.repo, tt.args.commit, tt.args.file, tt.args.line); got != tt.want { + t.Errorf("generateLink() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/sources/git/git.go b/pkg/sources/git/git.go index 16a2e64d3..9bed59ca6 100644 --- a/pkg/sources/git/git.go +++ b/pkg/sources/git/git.go @@ -9,7 +9,6 @@ import ( "os/exec" "path/filepath" "runtime" - "strconv" "strings" "sync/atomic" "time" @@ -693,28 +692,6 @@ func normalizeConfig(scanOptions *ScanOptions, repo *git.Repository) (err error) return nil } -// GenerateLink crafts a link to the specific file from a commit. This works in most major git providers (Github/Gitlab) -func GenerateLink(repo, commit, file string, line int64) string { - // bitbucket links are commits not commit... - if strings.Contains(repo, "bitbucket.org/") { - return repo[:len(repo)-4] + "/commits/" + commit - } - var link string - if file == "" { - link = repo[:len(repo)-4] + "/commit/" + commit - } else { - link = repo[:len(repo)-4] + "/blob/" + commit + "/" + file - - // Both GitHub and Gitlab support hyperlinking to a specific line with #L, e.g.: - // https://github.com/trufflesecurity/trufflehog/blob/e856a6890d0da5a218f4f9283500b80043884641/go.mod#L169 - // https://gitlab.com/pdftk-java/pdftk/-/blob/88559a08f34175b6fae76c40a88f0377f64a12d7/java/com/gitlab/pdftk_java/report.java#L893 - if line > 0 && (strings.Contains(repo, "github") || strings.Contains(repo, "gitlab")) { - link += "#L" + strconv.FormatInt(line, 10) - } - } - return link -} - func stripPassword(u string) (string, error) { if strings.HasPrefix(u, "git@") { return u, nil diff --git a/pkg/sources/git/git_test.go b/pkg/sources/git/git_test.go index d8d54adfd..15914b9ec 100644 --- a/pkg/sources/git/git_test.go +++ b/pkg/sources/git/git_test.go @@ -151,55 +151,6 @@ func TestSource_Scan(t *testing.T) { } } -func Test_generateLink(t *testing.T) { - type args struct { - repo string - commit string - file string - line int64 - } - tests := []struct { - name string - args args - want string - }{ - { - name: "test link gen", - args: args{ - repo: "https://github.com/trufflesec-julian/confluence-go-api.git", - commit: "047b4a2ba42fc5b6c0bd535c5307434a666db5ec", - file: ".gitignore", - }, - want: "https://github.com/trufflesec-julian/confluence-go-api/blob/047b4a2ba42fc5b6c0bd535c5307434a666db5ec/.gitignore", - }, - { - name: "test link gen", - args: args{ - repo: "https://github.com/trufflesec-julian/confluence-go-api.git", - commit: "047b4a2ba42fc5b6c0bd535c5307434a666db5ec", - file: ".gitignore", - line: int64(4), - }, - want: "https://github.com/trufflesec-julian/confluence-go-api/blob/047b4a2ba42fc5b6c0bd535c5307434a666db5ec/.gitignore#L4", - }, - { - name: "test link gen - no file", - args: args{ - repo: "https://github.com/trufflesec-julian/confluence-go-api.git", - commit: "047b4a2ba42fc5b6c0bd535c5307434a666db5ec", - }, - want: "https://github.com/trufflesec-julian/confluence-go-api/commit/047b4a2ba42fc5b6c0bd535c5307434a666db5ec", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := GenerateLink(tt.args.repo, tt.args.commit, tt.args.file, tt.args.line); got != tt.want { - t.Errorf("generateLink() = %v, want %v", got, tt.want) - } - }) - } -} - // We ran into an issue where upgrading a dependency caused the git patch chunking to break // So this test exists to make sure that when something changes, we know about it. func TestSource_Chunks_Integration(t *testing.T) { diff --git a/pkg/sources/github/github.go b/pkg/sources/github/github.go index 0f54f1249..de4508e3f 100644 --- a/pkg/sources/github/github.go +++ b/pkg/sources/github/github.go @@ -29,6 +29,7 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/cache/memory" "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/context" + "github.com/trufflesecurity/trufflehog/v3/pkg/giturl" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb" @@ -261,7 +262,7 @@ func (s *Source) Init(aCtx context.Context, name string, jobID, sourceID int64, File: sanitizer.UTF8(file), Email: sanitizer.UTF8(email), Repository: sanitizer.UTF8(repository), - Link: git.GenerateLink(repository, commit, file, line), + Link: giturl.GenerateLink(repository, commit, file, line), Timestamp: sanitizer.UTF8(timestamp), Line: line, Visibility: s.visibilityOf(aCtx, repository), diff --git a/pkg/sources/gitlab/gitlab.go b/pkg/sources/gitlab/gitlab.go index 9ce1057ff..9cfacea4d 100644 --- a/pkg/sources/gitlab/gitlab.go +++ b/pkg/sources/gitlab/gitlab.go @@ -127,7 +127,7 @@ func (s *Source) Init(_ context.Context, name string, jobId, sourceId int64, ver File: sanitizer.UTF8(file), Email: sanitizer.UTF8(email), Repository: sanitizer.UTF8(repository), - Link: git.GenerateLink(repository, commit, file, line), + Link: giturl.GenerateLink(repository, commit, file, line), Timestamp: sanitizer.UTF8(timestamp), Line: line, },