4
0

allowlist.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. // commitMap is a normalized version of Commits, used for efficiency purposes.
  48. // TODO: possible optimizations so that both short and long hashes work.
  49. commitMap map[string]struct{}
  50. regexPat *regexp.Regexp
  51. pathPat *regexp.Regexp
  52. stopwordTrie *ahocorasick.Trie
  53. }
  54. func (a *Allowlist) Validate() error {
  55. if a.validated {
  56. return nil
  57. }
  58. // Disallow empty allowlists.
  59. if len(a.Commits) == 0 &&
  60. len(a.Paths) == 0 &&
  61. len(a.Regexes) == 0 &&
  62. len(a.StopWords) == 0 {
  63. return errors.New("must contain at least one check for: commits, paths, regexes, or stopwords")
  64. }
  65. // Deduplicate commits and stopwords.
  66. if len(a.Commits) > 0 {
  67. uniqueCommits := make(map[string]struct{})
  68. for _, commit := range a.Commits {
  69. // Commits are case-insensitive.
  70. uniqueCommits[strings.TrimSpace(strings.ToLower(commit))] = struct{}{}
  71. }
  72. a.Commits = maps.Keys(uniqueCommits)
  73. a.commitMap = uniqueCommits
  74. }
  75. if len(a.StopWords) > 0 {
  76. uniqueStopwords := make(map[string]struct{})
  77. for _, stopWord := range a.StopWords {
  78. uniqueStopwords[strings.ToLower(stopWord)] = struct{}{}
  79. }
  80. values := maps.Keys(uniqueStopwords)
  81. a.StopWords = values
  82. a.stopwordTrie = ahocorasick.NewTrieBuilder().AddStrings(values).Build()
  83. }
  84. // Combine patterns into a single expression.
  85. if len(a.Paths) > 0 {
  86. a.pathPat = joinRegexOr(a.Paths)
  87. }
  88. if len(a.Regexes) > 0 {
  89. a.regexPat = joinRegexOr(a.Regexes)
  90. }
  91. a.validated = true
  92. return nil
  93. }
  94. // CommitAllowed returns true if the commit is allowed to be ignored.
  95. func (a *Allowlist) CommitAllowed(c string) (bool, string) {
  96. if a == nil || c == "" {
  97. return false, ""
  98. }
  99. if a.commitMap != nil {
  100. if _, ok := a.commitMap[strings.ToLower(c)]; ok {
  101. return true, ""
  102. }
  103. } else if len(a.Commits) > 0 {
  104. for _, commit := range a.Commits {
  105. if commit == c {
  106. return true, c
  107. }
  108. }
  109. }
  110. return false, ""
  111. }
  112. // PathAllowed returns true if the path is allowed to be ignored.
  113. func (a *Allowlist) PathAllowed(path string) bool {
  114. if a == nil || path == "" {
  115. return false
  116. }
  117. if a.pathPat != nil {
  118. return a.pathPat.MatchString(path)
  119. } else if len(a.Paths) > 0 {
  120. return anyRegexMatch(path, a.Paths)
  121. }
  122. return false
  123. }
  124. // RegexAllowed returns true if the regex is allowed to be ignored.
  125. func (a *Allowlist) RegexAllowed(secret string) bool {
  126. if a == nil || secret == "" {
  127. return false
  128. }
  129. if a.regexPat != nil {
  130. return a.regexPat.MatchString(secret)
  131. } else if len(a.Regexes) > 0 {
  132. return anyRegexMatch(secret, a.Regexes)
  133. }
  134. return false
  135. }
  136. func (a *Allowlist) ContainsStopWord(s string) (bool, string) {
  137. if a == nil || s == "" {
  138. return false, ""
  139. }
  140. s = strings.ToLower(s)
  141. if a.stopwordTrie != nil {
  142. if m := a.stopwordTrie.MatchFirstString(s); m != nil {
  143. return true, m.MatchString()
  144. }
  145. } else if len(a.StopWords) > 0 {
  146. for _, stopWord := range a.StopWords {
  147. if strings.Contains(s, stopWord) {
  148. return true, stopWord
  149. }
  150. }
  151. }
  152. return false, ""
  153. }