allowlist.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package config
  2. import (
  3. "errors"
  4. "strings"
  5. ahocorasick "github.com/BobuSumisu/aho-corasick"
  6. "golang.org/x/exp/maps"
  7. "github.com/zricethezav/gitleaks/v8/regexp"
  8. )
  9. type AllowlistMatchCondition int
  10. const (
  11. AllowlistMatchOr AllowlistMatchCondition = iota
  12. AllowlistMatchAnd
  13. )
  14. func (a AllowlistMatchCondition) String() string {
  15. return [...]string{
  16. "OR",
  17. "AND",
  18. }[a]
  19. }
  20. // Allowlist allows a rule to be ignored for specific
  21. // regexes, paths, and/or commits
  22. type Allowlist struct {
  23. // Short human readable description of the allowlist.
  24. Description string
  25. // MatchCondition determines whether all criteria must match. Defaults to "OR".
  26. MatchCondition AllowlistMatchCondition
  27. // Commits is a slice of commit SHAs that are allowed to be ignored.
  28. Commits []string
  29. // Paths is a slice of path regular expressions that are allowed to be ignored.
  30. Paths []*regexp.Regexp
  31. // Can be `match` or `line`.
  32. //
  33. // If `match` the _Regexes_ will be tested against the match of the _Rule.Regex_.
  34. //
  35. // If `line` the _Regexes_ will be tested against the entire line.
  36. //
  37. // If RegexTarget is empty, it will be tested against the found secret.
  38. RegexTarget string
  39. // Regexes is slice of content regular expressions that are allowed to be ignored.
  40. Regexes []*regexp.Regexp
  41. // StopWords is a slice of stop words that are allowed to be ignored.
  42. // This targets the _secret_, not the content of the regex match like the
  43. // Regexes slice.
  44. StopWords []string
  45. // validated is an internal flag to track whether `Validate()` has been called.
  46. validated bool
  47. // EnableExperimentalOptimizations must be set prior to calling `Validate()`.
  48. // See: https://github.com/gitleaks/gitleaks/pull/1731
  49. //
  50. // NOTE: This flag may be removed in the future.
  51. EnableExperimentalOptimizations bool
  52. // commitMap is a normalized version of Commits, used for efficiency purposes.
  53. // TODO: possible optimizations so that both short and long hashes work.
  54. commitMap map[string]struct{}
  55. regexPat *regexp.Regexp
  56. pathPat *regexp.Regexp
  57. stopwordTrie *ahocorasick.Trie
  58. }
  59. func (a *Allowlist) Validate() error {
  60. if a.validated {
  61. return nil
  62. }
  63. // Disallow empty allowlists.
  64. if len(a.Commits) == 0 &&
  65. len(a.Paths) == 0 &&
  66. len(a.Regexes) == 0 &&
  67. len(a.StopWords) == 0 {
  68. return errors.New("must contain at least one check for: commits, paths, regexes, or stopwords")
  69. }
  70. // Deduplicate commits and stopwords.
  71. if len(a.Commits) > 0 {
  72. uniqueCommits := make(map[string]struct{})
  73. for _, commit := range a.Commits {
  74. // Commits are case-insensitive.
  75. uniqueCommits[strings.TrimSpace(strings.ToLower(commit))] = struct{}{}
  76. }
  77. if a.EnableExperimentalOptimizations {
  78. a.commitMap = uniqueCommits
  79. } else {
  80. a.Commits = maps.Keys(uniqueCommits)
  81. }
  82. }
  83. if len(a.StopWords) > 0 {
  84. uniqueStopwords := make(map[string]struct{})
  85. for _, stopWord := range a.StopWords {
  86. uniqueStopwords[strings.ToLower(stopWord)] = struct{}{}
  87. }
  88. values := maps.Keys(uniqueStopwords)
  89. if a.EnableExperimentalOptimizations {
  90. a.stopwordTrie = ahocorasick.NewTrieBuilder().AddStrings(values).Build()
  91. } else {
  92. a.StopWords = values
  93. }
  94. }
  95. // Combine patterns into a single expression.
  96. if a.EnableExperimentalOptimizations {
  97. if len(a.Paths) > 0 {
  98. a.pathPat = joinRegexOr(a.Paths)
  99. }
  100. if len(a.Regexes) > 0 {
  101. a.regexPat = joinRegexOr(a.Regexes)
  102. }
  103. }
  104. a.validated = true
  105. return nil
  106. }
  107. // CommitAllowed returns true if the commit is allowed to be ignored.
  108. func (a *Allowlist) CommitAllowed(c string) (bool, string) {
  109. if a == nil || c == "" {
  110. return false, ""
  111. }
  112. if a.commitMap != nil {
  113. if _, ok := a.commitMap[strings.ToLower(c)]; ok {
  114. return true, ""
  115. }
  116. } else if len(a.Commits) > 0 {
  117. for _, commit := range a.Commits {
  118. if commit == c {
  119. return true, c
  120. }
  121. }
  122. }
  123. return false, ""
  124. }
  125. // PathAllowed returns true if the path is allowed to be ignored.
  126. func (a *Allowlist) PathAllowed(path string) bool {
  127. if a == nil || path == "" {
  128. return false
  129. }
  130. if a.pathPat != nil {
  131. return a.pathPat.MatchString(path)
  132. } else if len(a.Paths) > 0 {
  133. return anyRegexMatch(path, a.Paths)
  134. }
  135. return false
  136. }
  137. // RegexAllowed returns true if the regex is allowed to be ignored.
  138. func (a *Allowlist) RegexAllowed(secret string) bool {
  139. if a == nil || secret == "" {
  140. return false
  141. }
  142. if a.regexPat != nil {
  143. return a.regexPat.MatchString(secret)
  144. } else if len(a.Regexes) > 0 {
  145. return anyRegexMatch(secret, a.Regexes)
  146. }
  147. return false
  148. }
  149. func (a *Allowlist) ContainsStopWord(s string) (bool, string) {
  150. if a == nil || s == "" {
  151. return false, ""
  152. }
  153. s = strings.ToLower(s)
  154. if a.stopwordTrie != nil {
  155. if m := a.stopwordTrie.MatchFirstString(s); m != nil {
  156. return true, m.MatchString()
  157. }
  158. } else if len(a.StopWords) > 0 {
  159. for _, stopWord := range a.StopWords {
  160. if strings.Contains(s, stopWord) {
  161. return true, stopWord
  162. }
  163. }
  164. }
  165. return false, ""
  166. }