| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- package detect
- import (
- // "encoding/json"
- "fmt"
- "math"
- "path/filepath"
- "strings"
- "github.com/zricethezav/gitleaks/v8/cmd/scm"
- "github.com/zricethezav/gitleaks/v8/logging"
- "github.com/zricethezav/gitleaks/v8/report"
- "github.com/zricethezav/gitleaks/v8/sources"
- "github.com/charmbracelet/lipgloss"
- )
- var linkCleaner = strings.NewReplacer(
- " ", "%20",
- "%", "%25",
- )
- func createScmLink(remote *sources.RemoteInfo, finding report.Finding) string {
- if remote.Platform == scm.UnknownPlatform ||
- remote.Platform == scm.NoPlatform ||
- finding.Commit == "" {
- return ""
- }
- // Clean the path.
- filePath, _, hasInnerPath := strings.Cut(finding.File, sources.InnerPathSeparator)
- filePath = linkCleaner.Replace(filePath)
- switch remote.Platform {
- case scm.GitHubPlatform:
- link := fmt.Sprintf("%s/blob/%s/%s", remote.Url, finding.Commit, filePath)
- if hasInnerPath {
- return link
- }
- ext := strings.ToLower(filepath.Ext(filePath))
- if ext == ".ipynb" || ext == ".md" {
- link += "?plain=1"
- }
- if finding.StartLine != 0 {
- link += fmt.Sprintf("#L%d", finding.StartLine)
- }
- if finding.EndLine != finding.StartLine {
- link += fmt.Sprintf("-L%d", finding.EndLine)
- }
- return link
- case scm.GitLabPlatform:
- link := fmt.Sprintf("%s/blob/%s/%s", remote.Url, finding.Commit, filePath)
- if hasInnerPath {
- return link
- }
- if finding.StartLine != 0 {
- link += fmt.Sprintf("#L%d", finding.StartLine)
- }
- if finding.EndLine != finding.StartLine {
- link += fmt.Sprintf("-%d", finding.EndLine)
- }
- return link
- case scm.AzureDevOpsPlatform:
- link := fmt.Sprintf("%s/commit/%s?path=/%s", remote.Url, finding.Commit, filePath)
- // Add line information if applicable
- if hasInnerPath {
- return link
- }
- if finding.StartLine != 0 {
- link += fmt.Sprintf("&line=%d", finding.StartLine)
- }
- if finding.EndLine != finding.StartLine {
- link += fmt.Sprintf("&lineEnd=%d", finding.EndLine)
- }
- // This is a bit dirty, but Azure DevOps does not highlight the line when the lineStartColumn and lineEndColumn are not provided
- link += "&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files"
- return link
- case scm.GiteaPlatform:
- link := fmt.Sprintf("%s/src/commit/%s/%s", remote.Url, finding.Commit, filePath)
- if hasInnerPath {
- return link
- }
- ext := strings.ToLower(filepath.Ext(filePath))
- if ext == ".ipynb" || ext == ".md" {
- link += "?display=source"
- }
- if finding.StartLine != 0 {
- link += fmt.Sprintf("#L%d", finding.StartLine)
- }
- if finding.EndLine != finding.StartLine {
- link += fmt.Sprintf("-L%d", finding.EndLine)
- }
- return link
- case scm.BitbucketPlatform:
- link := fmt.Sprintf("%s/src/%s/%s", remote.Url, finding.Commit, filePath)
- if hasInnerPath {
- return link
- }
- if finding.StartLine != 0 {
- link += fmt.Sprintf("#lines-%d", finding.StartLine)
- }
- if finding.EndLine != finding.StartLine {
- link += fmt.Sprintf(":%d", finding.EndLine)
- }
- return link
- default:
- // This should never happen.
- return ""
- }
- }
- // shannonEntropy calculates the entropy of data using the formula defined here:
- // https://en.wiktionary.org/wiki/Shannon_entropy
- // Another way to think about what this is doing is calculating the number of bits
- // needed to on average encode the data. So, the higher the entropy, the more random the data, the
- // more bits needed to encode that data.
- func shannonEntropy(data string) (entropy float64) {
- if data == "" {
- return 0
- }
- charCounts := make(map[rune]int)
- for _, char := range data {
- charCounts[char]++
- }
- invLength := 1.0 / float64(len(data))
- for _, count := range charCounts {
- freq := float64(count) * invLength
- entropy -= freq * math.Log2(freq)
- }
- return entropy
- }
- // filter will dedupe and redact findings
- func filter(findings []report.Finding, redact uint) []report.Finding {
- var retFindings []report.Finding
- for _, f := range findings {
- include := true
- if strings.Contains(strings.ToLower(f.RuleID), "generic") {
- for _, fPrime := range findings {
- if f.StartLine == fPrime.StartLine &&
- f.Commit == fPrime.Commit &&
- f.RuleID != fPrime.RuleID &&
- strings.Contains(fPrime.Secret, f.Secret) &&
- !strings.Contains(strings.ToLower(fPrime.RuleID), "generic") {
- genericMatch := strings.ReplaceAll(f.Match, f.Secret, "REDACTED")
- betterMatch := strings.ReplaceAll(fPrime.Match, fPrime.Secret, "REDACTED")
- logging.Trace().Msgf("skipping %s finding (%s), %s rule takes precedence (%s)", f.RuleID, genericMatch, fPrime.RuleID, betterMatch)
- include = false
- break
- }
- }
- }
- if redact > 0 {
- f.Redact(redact)
- }
- if include {
- retFindings = append(retFindings, f)
- }
- }
- return retFindings
- }
- func printFinding(f report.Finding, noColor bool) {
- // trim all whitespace and tabs
- f.Line = strings.TrimSpace(f.Line)
- f.Secret = strings.TrimSpace(f.Secret)
- f.Match = strings.TrimSpace(f.Match)
- isFileMatch := strings.HasPrefix(f.Match, "file detected:")
- skipColor := noColor
- finding := ""
- var secret lipgloss.Style
- // Matches from filenames do not have a |line| or |secret|
- if !isFileMatch {
- matchInLineIDX := strings.Index(f.Line, f.Match)
- secretInMatchIdx := strings.Index(f.Match, f.Secret)
- skipColor = false
- if matchInLineIDX == -1 || noColor {
- skipColor = true
- matchInLineIDX = 0
- }
- start := f.Line[0:matchInLineIDX]
- startMatchIdx := 0
- if matchInLineIDX > 20 {
- startMatchIdx = matchInLineIDX - 20
- start = "..." + f.Line[startMatchIdx:matchInLineIDX]
- }
- matchBeginning := lipgloss.NewStyle().SetString(f.Match[0:secretInMatchIdx]).Foreground(lipgloss.Color("#f5d445"))
- secret = lipgloss.NewStyle().SetString(f.Secret).
- Bold(true).
- Italic(true).
- Foreground(lipgloss.Color("#f05c07"))
- matchEnd := lipgloss.NewStyle().SetString(f.Match[secretInMatchIdx+len(f.Secret):]).Foreground(lipgloss.Color("#f5d445"))
- lineEndIdx := matchInLineIDX + len(f.Match)
- if len(f.Line)-1 <= lineEndIdx {
- lineEndIdx = len(f.Line)
- }
- lineEnd := f.Line[lineEndIdx:]
- if len(f.Secret) > 100 {
- secret = lipgloss.NewStyle().SetString(f.Secret[0:100] + "...").
- Bold(true).
- Italic(true).
- Foreground(lipgloss.Color("#f05c07"))
- }
- if len(lineEnd) > 20 {
- lineEnd = lineEnd[0:20] + "..."
- }
- finding = fmt.Sprintf("%s%s%s%s%s\n", strings.TrimPrefix(strings.TrimLeft(start, " "), "\n"), matchBeginning, secret, matchEnd, lineEnd)
- }
- if skipColor || isFileMatch {
- fmt.Printf("%-12s %s\n", "Finding:", f.Match)
- fmt.Printf("%-12s %s\n", "Secret:", f.Secret)
- } else {
- fmt.Printf("%-12s %s", "Finding:", finding)
- fmt.Printf("%-12s %s\n", "Secret:", secret)
- }
- fmt.Printf("%-12s %s\n", "RuleID:", f.RuleID)
- fmt.Printf("%-12s %f\n", "Entropy:", f.Entropy)
- if f.File == "" {
- f.PrintRequiredFindings()
- fmt.Println("")
- return
- }
- if len(f.Tags) > 0 {
- fmt.Printf("%-12s %s\n", "Tags:", f.Tags)
- }
- fmt.Printf("%-12s %s\n", "File:", f.File)
- fmt.Printf("%-12s %d\n", "Line:", f.StartLine)
- if f.Commit == "" {
- fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
- f.PrintRequiredFindings()
- fmt.Println("")
- return
- }
- fmt.Printf("%-12s %s\n", "Commit:", f.Commit)
- fmt.Printf("%-12s %s\n", "Author:", f.Author)
- fmt.Printf("%-12s %s\n", "Email:", f.Email)
- fmt.Printf("%-12s %s\n", "Date:", f.Date)
- fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
- if f.Link != "" {
- fmt.Printf("%-12s %s\n", "Link:", f.Link)
- }
- f.PrintRequiredFindings()
- fmt.Println("")
- }
|