utils.go 7.3 KB

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