utils.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package detect
  2. import (
  3. // "encoding/json"
  4. "fmt"
  5. "math"
  6. "path/filepath"
  7. "strings"
  8. "time"
  9. "github.com/zricethezav/gitleaks/v8/cmd/scm"
  10. "github.com/zricethezav/gitleaks/v8/logging"
  11. "github.com/zricethezav/gitleaks/v8/report"
  12. "github.com/charmbracelet/lipgloss"
  13. "github.com/gitleaks/go-gitdiff/gitdiff"
  14. )
  15. // augmentGitFinding updates the start and end line numbers of a finding to include the
  16. // delta from the git diff
  17. func augmentGitFinding(remote *RemoteInfo, finding report.Finding, textFragment *gitdiff.TextFragment, f *gitdiff.File) report.Finding {
  18. if !strings.HasPrefix(finding.Match, "file detected") {
  19. finding.StartLine += int(textFragment.NewPosition)
  20. finding.EndLine += int(textFragment.NewPosition)
  21. }
  22. if f.PatchHeader != nil {
  23. finding.Commit = f.PatchHeader.SHA
  24. if f.PatchHeader.Author != nil {
  25. finding.Author = f.PatchHeader.Author.Name
  26. finding.Email = f.PatchHeader.Author.Email
  27. }
  28. finding.Date = f.PatchHeader.AuthorDate.UTC().Format(time.RFC3339)
  29. finding.Message = f.PatchHeader.Message()
  30. // Results from `git diff` shouldn't have a link.
  31. if finding.Commit != "" {
  32. finding.Link = createScmLink(remote.Platform, remote.Url, finding)
  33. }
  34. }
  35. return finding
  36. }
  37. var linkCleaner = strings.NewReplacer(
  38. " ", "%20",
  39. "%", "%25",
  40. )
  41. func createScmLink(scmPlatform scm.Platform, remoteUrl string, finding report.Finding) string {
  42. if scmPlatform == scm.UnknownPlatform || scmPlatform == scm.NoPlatform {
  43. return ""
  44. }
  45. // Clean the path.
  46. var (
  47. filePath = linkCleaner.Replace(finding.File)
  48. ext = strings.ToLower(filepath.Ext(filePath))
  49. )
  50. switch scmPlatform {
  51. case scm.GitHubPlatform:
  52. link := fmt.Sprintf("%s/blob/%s/%s", remoteUrl, finding.Commit, filePath)
  53. if ext == ".ipynb" || ext == ".md" {
  54. link += "?plain=1"
  55. }
  56. if finding.StartLine != 0 {
  57. link += fmt.Sprintf("#L%d", finding.StartLine)
  58. }
  59. if finding.EndLine != finding.StartLine {
  60. link += fmt.Sprintf("-L%d", finding.EndLine)
  61. }
  62. return link
  63. case scm.GitLabPlatform:
  64. link := fmt.Sprintf("%s/blob/%s/%s", remoteUrl, finding.Commit, filePath)
  65. if finding.StartLine != 0 {
  66. link += fmt.Sprintf("#L%d", finding.StartLine)
  67. }
  68. if finding.EndLine != finding.StartLine {
  69. link += fmt.Sprintf("-%d", finding.EndLine)
  70. }
  71. return link
  72. case scm.AzureDevOpsPlatform:
  73. link := fmt.Sprintf("%s/commit/%s?path=/%s", remoteUrl, finding.Commit, filePath)
  74. // Add line information if applicable
  75. if finding.StartLine != 0 {
  76. link += fmt.Sprintf("&line=%d", finding.StartLine)
  77. }
  78. if finding.EndLine != finding.StartLine {
  79. link += fmt.Sprintf("&lineEnd=%d", finding.EndLine)
  80. }
  81. // This is a bit dirty, but Azure DevOps does not highlight the line when the lineStartColumn and lineEndColumn are not provided
  82. link += "&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files"
  83. return link
  84. default:
  85. // This should never happen.
  86. return ""
  87. }
  88. }
  89. // shannonEntropy calculates the entropy of data using the formula defined here:
  90. // https://en.wiktionary.org/wiki/Shannon_entropy
  91. // Another way to think about what this is doing is calculating the number of bits
  92. // needed to on average encode the data. So, the higher the entropy, the more random the data, the
  93. // more bits needed to encode that data.
  94. func shannonEntropy(data string) (entropy float64) {
  95. if data == "" {
  96. return 0
  97. }
  98. charCounts := make(map[rune]int)
  99. for _, char := range data {
  100. charCounts[char]++
  101. }
  102. invLength := 1.0 / float64(len(data))
  103. for _, count := range charCounts {
  104. freq := float64(count) * invLength
  105. entropy -= freq * math.Log2(freq)
  106. }
  107. return entropy
  108. }
  109. // filter will dedupe and redact findings
  110. func filter(findings []report.Finding, redact uint) []report.Finding {
  111. var retFindings []report.Finding
  112. for _, f := range findings {
  113. include := true
  114. if strings.Contains(strings.ToLower(f.RuleID), "generic") {
  115. for _, fPrime := range findings {
  116. if f.StartLine == fPrime.StartLine &&
  117. f.Commit == fPrime.Commit &&
  118. f.RuleID != fPrime.RuleID &&
  119. strings.Contains(fPrime.Secret, f.Secret) &&
  120. !strings.Contains(strings.ToLower(fPrime.RuleID), "generic") {
  121. genericMatch := strings.Replace(f.Match, f.Secret, "REDACTED", -1)
  122. betterMatch := strings.Replace(fPrime.Match, fPrime.Secret, "REDACTED", -1)
  123. logging.Trace().Msgf("skipping %s finding (%s), %s rule takes precedence (%s)", f.RuleID, genericMatch, fPrime.RuleID, betterMatch)
  124. include = false
  125. break
  126. }
  127. }
  128. }
  129. if redact > 0 {
  130. f.Redact(redact)
  131. }
  132. if include {
  133. retFindings = append(retFindings, f)
  134. }
  135. }
  136. return retFindings
  137. }
  138. func printFinding(f report.Finding, noColor bool) {
  139. // trim all whitespace and tabs
  140. f.Line = strings.TrimSpace(f.Line)
  141. f.Secret = strings.TrimSpace(f.Secret)
  142. f.Match = strings.TrimSpace(f.Match)
  143. isFileMatch := strings.HasPrefix(f.Match, "file detected:")
  144. skipColor := noColor
  145. finding := ""
  146. var secret lipgloss.Style
  147. // Matches from filenames do not have a |line| or |secret|
  148. if !isFileMatch {
  149. matchInLineIDX := strings.Index(f.Line, f.Match)
  150. secretInMatchIdx := strings.Index(f.Match, f.Secret)
  151. skipColor = false
  152. if matchInLineIDX == -1 || noColor {
  153. skipColor = true
  154. matchInLineIDX = 0
  155. }
  156. start := f.Line[0:matchInLineIDX]
  157. startMatchIdx := 0
  158. if matchInLineIDX > 20 {
  159. startMatchIdx = matchInLineIDX - 20
  160. start = "..." + f.Line[startMatchIdx:matchInLineIDX]
  161. }
  162. matchBeginning := lipgloss.NewStyle().SetString(f.Match[0:secretInMatchIdx]).Foreground(lipgloss.Color("#f5d445"))
  163. secret = lipgloss.NewStyle().SetString(f.Secret).
  164. Bold(true).
  165. Italic(true).
  166. Foreground(lipgloss.Color("#f05c07"))
  167. matchEnd := lipgloss.NewStyle().SetString(f.Match[secretInMatchIdx+len(f.Secret):]).Foreground(lipgloss.Color("#f5d445"))
  168. lineEndIdx := matchInLineIDX + len(f.Match)
  169. if len(f.Line)-1 <= lineEndIdx {
  170. lineEndIdx = len(f.Line)
  171. }
  172. lineEnd := f.Line[lineEndIdx:]
  173. if len(f.Secret) > 100 {
  174. secret = lipgloss.NewStyle().SetString(f.Secret[0:100] + "...").
  175. Bold(true).
  176. Italic(true).
  177. Foreground(lipgloss.Color("#f05c07"))
  178. }
  179. if len(lineEnd) > 20 {
  180. lineEnd = lineEnd[0:20] + "..."
  181. }
  182. finding = fmt.Sprintf("%s%s%s%s%s\n", strings.TrimPrefix(strings.TrimLeft(start, " "), "\n"), matchBeginning, secret, matchEnd, lineEnd)
  183. }
  184. if skipColor || isFileMatch {
  185. fmt.Printf("%-12s %s\n", "Finding:", f.Match)
  186. fmt.Printf("%-12s %s\n", "Secret:", f.Secret)
  187. } else {
  188. fmt.Printf("%-12s %s", "Finding:", finding)
  189. fmt.Printf("%-12s %s\n", "Secret:", secret)
  190. }
  191. fmt.Printf("%-12s %s\n", "RuleID:", f.RuleID)
  192. fmt.Printf("%-12s %f\n", "Entropy:", f.Entropy)
  193. if f.File == "" {
  194. fmt.Println("")
  195. return
  196. }
  197. if len(f.Tags) > 0 {
  198. fmt.Printf("%-12s %s\n", "Tags:", f.Tags)
  199. }
  200. fmt.Printf("%-12s %s\n", "File:", f.File)
  201. fmt.Printf("%-12s %d\n", "Line:", f.StartLine)
  202. if f.Commit == "" {
  203. fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
  204. fmt.Println("")
  205. return
  206. }
  207. fmt.Printf("%-12s %s\n", "Commit:", f.Commit)
  208. fmt.Printf("%-12s %s\n", "Author:", f.Author)
  209. fmt.Printf("%-12s %s\n", "Email:", f.Email)
  210. fmt.Printf("%-12s %s\n", "Date:", f.Date)
  211. fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
  212. if f.Link != "" {
  213. fmt.Printf("%-12s %s\n", "Link:", f.Link)
  214. }
  215. fmt.Println("")
  216. }
  217. func isWhitespace(ch byte) bool {
  218. return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
  219. }