detect.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. package detect
  2. import (
  3. "bufio"
  4. "context"
  5. "fmt"
  6. "os"
  7. "runtime"
  8. "strings"
  9. "sync"
  10. "sync/atomic"
  11. "time"
  12. "github.com/zricethezav/gitleaks/v8/config"
  13. "github.com/zricethezav/gitleaks/v8/logging"
  14. "github.com/zricethezav/gitleaks/v8/regexp"
  15. "github.com/zricethezav/gitleaks/v8/report"
  16. ahocorasick "github.com/BobuSumisu/aho-corasick"
  17. "github.com/fatih/semgroup"
  18. "github.com/rs/zerolog"
  19. "github.com/spf13/viper"
  20. "golang.org/x/exp/maps"
  21. )
  22. const (
  23. gitleaksAllowSignature = "gitleaks:allow"
  24. chunkSize = 100 * 1_000 // 100kb
  25. // SlowWarningThreshold is the amount of time to wait before logging that a file is slow.
  26. // This is useful for identifying problematic files and tuning the allowlist.
  27. SlowWarningThreshold = 5 * time.Second
  28. )
  29. var (
  30. newLineRegexp = regexp.MustCompile("\n")
  31. isWindows = runtime.GOOS == "windows"
  32. )
  33. // Detector is the main detector struct
  34. type Detector struct {
  35. // Config is the configuration for the detector
  36. Config config.Config
  37. // Redact is a flag to redact findings. This is exported
  38. // so users using gitleaks as a library can set this flag
  39. // without calling `detector.Start(cmd *cobra.Command)`
  40. Redact uint
  41. // verbose is a flag to print findings
  42. Verbose bool
  43. // MaxDecodeDepths limits how many recursive decoding passes are allowed
  44. MaxDecodeDepth int
  45. // files larger than this will be skipped
  46. MaxTargetMegaBytes int
  47. // followSymlinks is a flag to enable scanning symlink files
  48. FollowSymlinks bool
  49. // NoColor is a flag to disable color output
  50. NoColor bool
  51. // IgnoreGitleaksAllow is a flag to ignore gitleaks:allow comments.
  52. IgnoreGitleaksAllow bool
  53. // commitMap is used to keep track of commits that have been scanned.
  54. // This is only used for logging purposes and git scans.
  55. commitMap map[string]bool
  56. // findingMutex is to prevent concurrent access to the
  57. // findings slice when adding findings.
  58. findingMutex *sync.Mutex
  59. // findings is a slice of report.Findings. This is the result
  60. // of the detector's scan which can then be used to generate a
  61. // report.
  62. findings []report.Finding
  63. // prefilter is a ahocorasick struct used for doing efficient string
  64. // matching given a set of words (keywords from the rules in the config)
  65. prefilter ahocorasick.Trie
  66. // a list of known findings that should be ignored
  67. baseline []report.Finding
  68. // path to baseline
  69. baselinePath string
  70. // gitleaksIgnore
  71. gitleaksIgnore map[string]struct{}
  72. // Sema (https://github.com/fatih/semgroup) controls the concurrency
  73. Sema *semgroup.Group
  74. // report-related settings.
  75. ReportPath string
  76. Reporter report.Reporter
  77. TotalBytes atomic.Uint64
  78. }
  79. // Fragment contains the data to be scanned
  80. type Fragment struct {
  81. // Raw is the raw content of the fragment
  82. Raw string
  83. Bytes []byte
  84. // FilePath is the path to the file, if applicable.
  85. // The path separator MUST be normalized to `/`.
  86. FilePath string
  87. SymlinkFile string
  88. // WindowsFilePath is the path with the original separator.
  89. // This provides a backwards-compatible solution to https://github.com/gitleaks/gitleaks/issues/1565.
  90. WindowsFilePath string `json:"-"` // TODO: remove this in v9.
  91. // CommitSHA is the SHA of the commit if applicable
  92. CommitSHA string
  93. // newlineIndices is a list of indices of newlines in the raw content.
  94. // This is used to calculate the line location of a finding
  95. newlineIndices [][]int
  96. }
  97. // NewDetector creates a new detector with the given config
  98. func NewDetector(cfg config.Config) *Detector {
  99. return &Detector{
  100. commitMap: make(map[string]bool),
  101. gitleaksIgnore: make(map[string]struct{}),
  102. findingMutex: &sync.Mutex{},
  103. findings: make([]report.Finding, 0),
  104. Config: cfg,
  105. prefilter: *ahocorasick.NewTrieBuilder().AddStrings(maps.Keys(cfg.Keywords)).Build(),
  106. Sema: semgroup.NewGroup(context.Background(), 40),
  107. }
  108. }
  109. // NewDetectorDefaultConfig creates a new detector with the default config
  110. func NewDetectorDefaultConfig() (*Detector, error) {
  111. viper.SetConfigType("toml")
  112. err := viper.ReadConfig(strings.NewReader(config.DefaultConfig))
  113. if err != nil {
  114. return nil, err
  115. }
  116. var vc config.ViperConfig
  117. err = viper.Unmarshal(&vc)
  118. if err != nil {
  119. return nil, err
  120. }
  121. cfg, err := vc.Translate()
  122. if err != nil {
  123. return nil, err
  124. }
  125. return NewDetector(cfg), nil
  126. }
  127. func (d *Detector) AddGitleaksIgnore(gitleaksIgnorePath string) error {
  128. logging.Debug().Msgf("found .gitleaksignore file: %s", gitleaksIgnorePath)
  129. file, err := os.Open(gitleaksIgnorePath)
  130. if err != nil {
  131. return err
  132. }
  133. defer func() {
  134. // https://github.com/securego/gosec/issues/512
  135. if err := file.Close(); err != nil {
  136. logging.Warn().Msgf("Error closing .gitleaksignore file: %s\n", err)
  137. }
  138. }()
  139. scanner := bufio.NewScanner(file)
  140. replacer := strings.NewReplacer("\\", "/")
  141. for scanner.Scan() {
  142. line := strings.TrimSpace(scanner.Text())
  143. // Skip lines that start with a comment
  144. if line == "" || strings.HasPrefix(line, "#") {
  145. continue
  146. }
  147. // Normalize the path.
  148. // TODO: Make this a breaking change in v9.
  149. s := strings.Split(line, ":")
  150. switch len(s) {
  151. case 3:
  152. // Global fingerprint.
  153. // `file:rule-id:start-line`
  154. s[0] = replacer.Replace(s[0])
  155. case 4:
  156. // Commit fingerprint.
  157. // `commit:file:rule-id:start-line`
  158. s[1] = replacer.Replace(s[1])
  159. default:
  160. logging.Warn().Str("fingerprint", line).Msg("Invalid .gitleaksignore entry")
  161. }
  162. d.gitleaksIgnore[strings.Join(s, ":")] = struct{}{}
  163. }
  164. return nil
  165. }
  166. // DetectBytes scans the given bytes and returns a list of findings
  167. func (d *Detector) DetectBytes(content []byte) []report.Finding {
  168. return d.DetectString(string(content))
  169. }
  170. // DetectString scans the given string and returns a list of findings
  171. func (d *Detector) DetectString(content string) []report.Finding {
  172. return d.Detect(Fragment{
  173. Raw: content,
  174. })
  175. }
  176. // Detect scans the given fragment and returns a list of findings
  177. func (d *Detector) Detect(fragment Fragment) []report.Finding {
  178. if fragment.Bytes == nil {
  179. d.TotalBytes.Add(uint64(len(fragment.Raw)))
  180. }
  181. d.TotalBytes.Add(uint64(len(fragment.Bytes)))
  182. var (
  183. findings []report.Finding
  184. logger = func() zerolog.Logger {
  185. l := logging.With().Str("path", fragment.FilePath)
  186. if fragment.CommitSHA != "" {
  187. l = l.Str("commit", fragment.CommitSHA)
  188. }
  189. return l.Logger()
  190. }()
  191. )
  192. // check if filepath is allowed
  193. if fragment.FilePath != "" {
  194. // is the path our config or baseline file?
  195. if fragment.FilePath == d.Config.Path || (d.baselinePath != "" && fragment.FilePath == d.baselinePath) {
  196. logging.Trace().Msg("skipping file: matches config or baseline path")
  197. return findings
  198. }
  199. }
  200. // check if commit or filepath is allowed.
  201. if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, d.Config.Allowlists); isAllowed {
  202. event.Msg("skipping file: global allowlist")
  203. return findings
  204. }
  205. // add newline indices for location calculation in detectRule
  206. fragment.newlineIndices = newLineRegexp.FindAllStringIndex(fragment.Raw, -1)
  207. // setup variables to handle different decoding passes
  208. currentRaw := fragment.Raw
  209. encodedSegments := []EncodedSegment{}
  210. currentDecodeDepth := 0
  211. decoder := NewDecoder()
  212. for {
  213. // build keyword map for prefiltering rules
  214. keywords := make(map[string]bool)
  215. normalizedRaw := strings.ToLower(currentRaw)
  216. matches := d.prefilter.MatchString(normalizedRaw)
  217. for _, m := range matches {
  218. keywords[normalizedRaw[m.Pos():int(m.Pos())+len(m.Match())]] = true
  219. }
  220. for _, rule := range d.Config.Rules {
  221. if len(rule.Keywords) == 0 {
  222. // if no keywords are associated with the rule always scan the
  223. // fragment using the rule
  224. findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...)
  225. continue
  226. }
  227. // check if keywords are in the fragment
  228. for _, k := range rule.Keywords {
  229. if _, ok := keywords[strings.ToLower(k)]; ok {
  230. findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...)
  231. break
  232. }
  233. }
  234. }
  235. // increment the depth by 1 as we start our decoding pass
  236. currentDecodeDepth++
  237. // stop the loop if we've hit our max decoding depth
  238. if currentDecodeDepth > d.MaxDecodeDepth {
  239. break
  240. }
  241. // decode the currentRaw for the next pass
  242. currentRaw, encodedSegments = decoder.decode(currentRaw, encodedSegments)
  243. // stop the loop when there's nothing else to decode
  244. if len(encodedSegments) == 0 {
  245. break
  246. }
  247. }
  248. return filter(findings, d.Redact)
  249. }
  250. // detectRule scans the given fragment for the given rule and returns a list of findings
  251. func (d *Detector) detectRule(fragment Fragment, currentRaw string, r config.Rule, encodedSegments []EncodedSegment) []report.Finding {
  252. var (
  253. findings []report.Finding
  254. logger = func() zerolog.Logger {
  255. l := logging.With().Str("rule-id", r.RuleID).Str("path", fragment.FilePath)
  256. if fragment.CommitSHA != "" {
  257. l = l.Str("commit", fragment.CommitSHA)
  258. }
  259. return l.Logger()
  260. }()
  261. )
  262. // check if commit or file is allowed for this rule.
  263. if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, r.Allowlists); isAllowed {
  264. event.Msg("skipping file: rule allowlist")
  265. return findings
  266. }
  267. if r.Path != nil {
  268. if r.Regex == nil && len(encodedSegments) == 0 {
  269. // Path _only_ rule
  270. if r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath)) {
  271. finding := report.Finding{
  272. RuleID: r.RuleID,
  273. Description: r.Description,
  274. File: fragment.FilePath,
  275. SymlinkFile: fragment.SymlinkFile,
  276. Match: fmt.Sprintf("file detected: %s", fragment.FilePath),
  277. Tags: r.Tags,
  278. }
  279. return append(findings, finding)
  280. }
  281. } else {
  282. // if path is set _and_ a regex is set, then we need to check both
  283. // so if the path does not match, then we should return early and not
  284. // consider the regex
  285. if !(r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath))) {
  286. return findings
  287. }
  288. }
  289. }
  290. // if path only rule, skip content checks
  291. if r.Regex == nil {
  292. return findings
  293. }
  294. // if flag configure and raw data size bigger then the flag
  295. if d.MaxTargetMegaBytes > 0 {
  296. rawLength := len(currentRaw) / 1000000
  297. if rawLength > d.MaxTargetMegaBytes {
  298. logger.Debug().
  299. Int("size", rawLength).
  300. Int("max-size", d.MaxTargetMegaBytes).
  301. Msg("skipping fragment: size")
  302. return findings
  303. }
  304. }
  305. // use currentRaw instead of fragment.Raw since this represents the current
  306. // decoding pass on the text
  307. for _, matchIndex := range r.Regex.FindAllStringIndex(currentRaw, -1) {
  308. // Extract secret from match
  309. secret := strings.Trim(currentRaw[matchIndex[0]:matchIndex[1]], "\n")
  310. // For any meta data from decoding
  311. var metaTags []string
  312. currentLine := ""
  313. // Check if the decoded portions of the segment overlap with the match
  314. // to see if its potentially a new match
  315. if len(encodedSegments) > 0 {
  316. if segment := segmentWithDecodedOverlap(encodedSegments, matchIndex[0], matchIndex[1]); segment != nil {
  317. matchIndex = segment.adjustMatchIndex(matchIndex)
  318. metaTags = append(metaTags, segment.tags()...)
  319. currentLine = segment.currentLine(currentRaw)
  320. } else {
  321. // This item has already been added to a finding
  322. continue
  323. }
  324. } else {
  325. // Fixes: https://github.com/gitleaks/gitleaks/issues/1352
  326. // removes the incorrectly following line that was detected by regex expression '\n'
  327. matchIndex[1] = matchIndex[0] + len(secret)
  328. }
  329. // determine location of match. Note that the location
  330. // in the finding will be the line/column numbers of the _match_
  331. // not the _secret_, which will be different if the secretGroup
  332. // value is set for this rule
  333. loc := location(fragment, matchIndex)
  334. if matchIndex[1] > loc.endLineIndex {
  335. loc.endLineIndex = matchIndex[1]
  336. }
  337. finding := report.Finding{
  338. RuleID: r.RuleID,
  339. Description: r.Description,
  340. StartLine: loc.startLine,
  341. EndLine: loc.endLine,
  342. StartColumn: loc.startColumn,
  343. EndColumn: loc.endColumn,
  344. Line: fragment.Raw[loc.startLineIndex:loc.endLineIndex],
  345. Match: secret,
  346. Secret: secret,
  347. File: fragment.FilePath,
  348. SymlinkFile: fragment.SymlinkFile,
  349. Tags: append(r.Tags, metaTags...),
  350. }
  351. if !d.IgnoreGitleaksAllow && strings.Contains(finding.Line, gitleaksAllowSignature) {
  352. logger.Trace().
  353. Str("finding", finding.Secret).
  354. Msg("skipping finding: 'gitleaks:allow' signature")
  355. continue
  356. }
  357. if currentLine == "" {
  358. currentLine = finding.Line
  359. }
  360. // Set the value of |secret|, if the pattern contains at least one capture group.
  361. // (The first element is the full match, hence we check >= 2.)
  362. groups := r.Regex.FindStringSubmatch(finding.Secret)
  363. if len(groups) >= 2 {
  364. if r.SecretGroup > 0 {
  365. if len(groups) <= r.SecretGroup {
  366. // Config validation should prevent this
  367. continue
  368. }
  369. finding.Secret = groups[r.SecretGroup]
  370. } else {
  371. // If |secretGroup| is not set, we will use the first suitable capture group.
  372. for _, s := range groups[1:] {
  373. if len(s) > 0 {
  374. finding.Secret = s
  375. break
  376. }
  377. }
  378. }
  379. }
  380. // check entropy
  381. entropy := shannonEntropy(finding.Secret)
  382. finding.Entropy = float32(entropy)
  383. if r.Entropy != 0.0 {
  384. // entropy is too low, skip this finding
  385. if entropy <= r.Entropy {
  386. logger.Trace().
  387. Str("finding", finding.Secret).
  388. Float32("entropy", finding.Entropy).
  389. Msg("skipping finding: low entropy")
  390. continue
  391. }
  392. }
  393. // check if the result matches any of the global allowlists.
  394. if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, d.Config.Allowlists); isAllowed {
  395. event.Msg("skipping finding: global allowlist")
  396. continue
  397. }
  398. // check if the result matches any of the rule allowlists.
  399. if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, r.Allowlists); isAllowed {
  400. event.Msg("skipping finding: rule allowlist")
  401. continue
  402. }
  403. findings = append(findings, finding)
  404. }
  405. return findings
  406. }
  407. // AddFinding synchronously adds a finding to the findings slice
  408. func (d *Detector) AddFinding(finding report.Finding) {
  409. globalFingerprint := fmt.Sprintf("%s:%s:%d", finding.File, finding.RuleID, finding.StartLine)
  410. if finding.Commit != "" {
  411. finding.Fingerprint = fmt.Sprintf("%s:%s:%s:%d", finding.Commit, finding.File, finding.RuleID, finding.StartLine)
  412. } else {
  413. finding.Fingerprint = globalFingerprint
  414. }
  415. // check if we should ignore this finding
  416. logger := logging.With().Str("finding", finding.Secret).Logger()
  417. if _, ok := d.gitleaksIgnore[globalFingerprint]; ok {
  418. logger.Debug().
  419. Str("fingerprint", globalFingerprint).
  420. Msg("skipping finding: global fingerprint")
  421. return
  422. } else if finding.Commit != "" {
  423. // Awkward nested if because I'm not sure how to chain these two conditions.
  424. if _, ok := d.gitleaksIgnore[finding.Fingerprint]; ok {
  425. logger.Debug().
  426. Str("fingerprint", finding.Fingerprint).
  427. Msgf("skipping finding: fingerprint")
  428. return
  429. }
  430. }
  431. if d.baseline != nil && !IsNew(finding, d.Redact, d.baseline) {
  432. logger.Debug().
  433. Str("fingerprint", finding.Fingerprint).
  434. Msgf("skipping finding: baseline")
  435. return
  436. }
  437. d.findingMutex.Lock()
  438. d.findings = append(d.findings, finding)
  439. if d.Verbose {
  440. printFinding(finding, d.NoColor)
  441. }
  442. d.findingMutex.Unlock()
  443. }
  444. // Findings returns the findings added to the detector
  445. func (d *Detector) Findings() []report.Finding {
  446. return d.findings
  447. }
  448. // AddCommit synchronously adds a commit to the commit slice
  449. func (d *Detector) addCommit(commit string) {
  450. d.commitMap[commit] = true
  451. }
  452. // checkCommitOrPathAllowed evaluates |fragment| against all provided |allowlists|.
  453. //
  454. // If the match condition is "OR", only commit and path are checked.
  455. // Otherwise, if regexes or stopwords are defined this will fail.
  456. func checkCommitOrPathAllowed(
  457. logger zerolog.Logger,
  458. fragment Fragment,
  459. allowlists []*config.Allowlist,
  460. ) (bool, *zerolog.Event) {
  461. if fragment.FilePath == "" && fragment.CommitSHA == "" {
  462. return false, nil
  463. }
  464. for _, a := range allowlists {
  465. var (
  466. isAllowed bool
  467. allowlistChecks []bool
  468. commitAllowed, _ = a.CommitAllowed(fragment.CommitSHA)
  469. pathAllowed = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath))
  470. )
  471. // If the condition is "AND" we need to check all conditions.
  472. if a.MatchCondition == config.AllowlistMatchAnd {
  473. if len(a.Commits) > 0 {
  474. allowlistChecks = append(allowlistChecks, commitAllowed)
  475. }
  476. if len(a.Paths) > 0 {
  477. allowlistChecks = append(allowlistChecks, pathAllowed)
  478. }
  479. // These will be checked later.
  480. if len(a.Regexes) > 0 {
  481. continue
  482. }
  483. if len(a.StopWords) > 0 {
  484. continue
  485. }
  486. isAllowed = allTrue(allowlistChecks)
  487. } else {
  488. isAllowed = commitAllowed || pathAllowed
  489. }
  490. if isAllowed {
  491. event := logger.Trace().Str("condition", a.MatchCondition.String())
  492. if commitAllowed {
  493. event.Bool("allowed-commit", commitAllowed)
  494. }
  495. if pathAllowed {
  496. event.Bool("allowed-path", pathAllowed)
  497. }
  498. return true, event
  499. }
  500. }
  501. return false, nil
  502. }
  503. // checkFindingAllowed evaluates |finding| against all provided |allowlists|.
  504. //
  505. // If the match condition is "OR", only regex and stopwords are run. (Commit and path should be handled separately).
  506. // Otherwise, all conditions are checked.
  507. //
  508. // TODO: The method signature is awkward. I can't think of a better way to log helpful info.
  509. func checkFindingAllowed(
  510. logger zerolog.Logger,
  511. finding report.Finding,
  512. fragment Fragment,
  513. currentLine string,
  514. allowlists []*config.Allowlist,
  515. ) (bool, *zerolog.Event) {
  516. for _, a := range allowlists {
  517. allowlistTarget := finding.Secret
  518. switch a.RegexTarget {
  519. case "match":
  520. allowlistTarget = finding.Match
  521. case "line":
  522. allowlistTarget = currentLine
  523. }
  524. var (
  525. checks []bool
  526. isAllowed bool
  527. commitAllowed bool
  528. commit string
  529. pathAllowed bool
  530. regexAllowed = a.RegexAllowed(allowlistTarget)
  531. containsStopword, word = a.ContainsStopWord(finding.Secret)
  532. )
  533. // If the condition is "AND" we need to check all conditions.
  534. if a.MatchCondition == config.AllowlistMatchAnd {
  535. // Determine applicable checks.
  536. if len(a.Commits) > 0 {
  537. commitAllowed, commit = a.CommitAllowed(fragment.CommitSHA)
  538. checks = append(checks, commitAllowed)
  539. }
  540. if len(a.Paths) > 0 {
  541. pathAllowed = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath))
  542. checks = append(checks, pathAllowed)
  543. }
  544. if len(a.Regexes) > 0 {
  545. checks = append(checks, regexAllowed)
  546. }
  547. if len(a.StopWords) > 0 {
  548. checks = append(checks, containsStopword)
  549. }
  550. isAllowed = allTrue(checks)
  551. } else {
  552. isAllowed = regexAllowed || containsStopword
  553. }
  554. if isAllowed {
  555. event := logger.Trace().
  556. Str("finding", finding.Secret).
  557. Str("condition", a.MatchCondition.String())
  558. if commitAllowed {
  559. event.Str("allowed-commit", commit)
  560. }
  561. if pathAllowed {
  562. event.Bool("allowed-path", pathAllowed)
  563. }
  564. if regexAllowed {
  565. event.Bool("allowed-regex", regexAllowed)
  566. }
  567. if containsStopword {
  568. event.Str("allowed-stopword", word)
  569. }
  570. return true, event
  571. }
  572. }
  573. return false, nil
  574. }
  575. func allTrue(bools []bool) bool {
  576. for _, check := range bools {
  577. if !check {
  578. return false
  579. }
  580. }
  581. return true
  582. }