8000
Skip to content

fix(sources): don't skip files on path match when allowlist condition is AND - #2227

Open
BasitS-hash wants to merge 1 commit into
gitleaks:masterfrom
BasitS-hash:fix/global-allowlist-and-condition-skips-files
Open

fix(sources): don't skip files on path match when allowlist condition is AND#2227
BasitS-hash wants to merge 1 commit into
gitleaks:masterfrom
BasitS-hash:fix/global-allowlist-and-condition-skips-files

Conversation

@BasitS-hash
Copy link
Copy Markdown

Problem

A global allowlist that uses condition = "AND" with both paths and regexes causes matching files to be skipped entirely during enumeration, before any content is read. Every secret in those files goes unreported.

[extend]
useDefault = true

[[allowlists]]
description = "ignore one placeholder, but only in docs"
condition = "AND"
paths   = ['''\.md$''']
regexes = ['''EXAMPLE_PLACEHOLDER_TOKEN''']

Against a directory whose notes.md contains a real AWS key, a high-entropy token, and a GitHub PAT — none of which match the allowlist regex:

$ gitleaks dir . --no-banner --redact          # no config
INF scanned ~140 bytes
WRN leaks found: 3

$ gitleaks dir . -c allow.toml --no-banner --redact
INF scanned ~0 bytes
INF no leaks found

scanned ~0 bytes — the file is never opened. The AND cannot possibly be satisfied ( 8000 the regex matches nothing in it), yet the file is dropped anyway.

Cause

shouldSkipPath in sources/common.go iterates the global allowlists and skips on PathAllowed alone, without consulting MatchCondition:

for _, a := range cfg.Allowlists {
	if a.PathAllowed(path) || (isWindows && a.PathAllowed(filepath.ToSlash(path))) {
		return true
	}
}

The detector already handles this correctly — detect.go checks config.AllowlistMatchAnd in both checkCommitOrPathAllowed (L784) and checkFindingAllowed (L849). So the two layers disagree, and the enumeration layer wins by dropping the file before the detector can evaluate the other half of the AND.

This seems worth treating as more than a cosmetic bug because the failure is silent and points the wrong way for a secret scanner: a config written to allowlist one pattern within some files instead stops those files being scanned at all. Nothing in the output distinguishes "clean" from "not looked at" other than the byte count.

Fix

Only skip on a path match when the path is sufficient on its own:

  • condition = "OR" (the default) — unchanged, a single criterion allowlists the finding, so skipping early is correct and stays a useful optimisation.
  • condition = "AND" — only skip when paths is the sole criterion. If regexes or stopwords are also set they are content-based and must reach the detector.

commits is deliberately not treated as content-based: it is known before a file is read, and the git source filters allowed commits separately.

Verification

config before after
paths only, default OR no leaks found no leaks found (unchanged)
paths + regexes, condition = "AND" no leaks found — 3 secrets missed leaks found: 3
  • go test -race ./... — 7 packages ok, 0 failures
  • go vet ./sources/ clean, gofmt clean
  • Added TestShouldSkipPath as a table test covering both conditions, the paths-only AND case, and the stopwords variant, plus a nil-config case

Note on the docs

The README documents condition under [[rules.allowlists]] but not under the global [[allowlists]] block, so it is easy to assume the field is rule-scoped. Happy to add that to the README in this PR or a separate one, whichever you prefer.

I ran into this while allowlisting placeholder bearer tokens in a project's docs — the scan went green and it took a planted-secret test to notice the files were not being read.

A global allowlist using `condition = "AND"` with both `paths` and
`regexes` currently causes matching files to be skipped entirely during
enumeration, before any content is read. Every secret in those files goes
unreported.

shouldSkipPath iterated the global allowlists and skipped on PathAllowed
alone, without consulting MatchCondition. The detector already handles
this correctly (detect.go checks AllowlistMatchAnd in both
checkCommitOrPathAllowed and checkFindingAllowed), so the two layers
disagreed: the file was dropped before the detector could evaluate the
other half of the AND.

The failure is silent and points the wrong way for a secret scanner — a
config written to allowlist one pattern within some files instead stops
those files being scanned at all.

Only skip on a path match when the path is sufficient on its own: under
"OR" (unchanged), or under "AND" when paths are the sole criterion.
Regexes and stopwords are content-based and have to reach the detector.
Commits are not treated as content-based, since they are known before a
file is read and the git source filters allowed commits separately.

Reproduction, against a directory holding a real AWS key, a high-entropy
token and a GitHub PAT:

    [[allowlists]]
    condition = "AND"
    paths   = ['''\.md$''']
    regexes = ['''EXAMPLE_PLACEHOLDER_TOKEN''']

  before: scanned ~0 bytes, no leaks found
  after:  leaks found: 3

The same config with the default "OR" condition still skips, as
documented.
Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a silent false-negative scenario in the file-enumeration layer where global allowlists using condition = "AND" could cause files to be skipped purely on a path match, preventing their contents from ever reaching the detector for content-based allowlist evaluation.

Changes:

  • Updates shouldSkipPath to only skip early on a path match when a path match is sufficient given the allowlist condition.
  • Introduces pathIsSufficientToSkip to centralize the “path-only is enough” logic for allowlists.
  • Adds table-driven tests covering OR vs AND behavior and a nil-config case.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
sources/common.go Prevents early skipping on AND allowlists that require content-based checks by gating path-only skipping logic.
sources/common_test.go Adds coverage to ensure enumeration-level skipping matches intended allowlist semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sources/common.go
Comment on lines +75 to +80
func pathIsSufficientToSkip(a *config.Allowlist) bool {
if a.MatchCondition != config.AllowlistMatchAnd {
return true
}
return len(a.Regexes) == 0 && len(a.StopWords) == 0
}
Comment thread sources/common_test.go
"bufio"
"bytes"
"io"
"regexp"
Comment thread sources/common_test.go
Comment on lines +136 to +144
"and condition, paths are the only criterion": {
// Nothing else has to be checked, so the path is sufficient.
allowlist: &config.Allowlist{
MatchCondition: config.AllowlistMatchAnd,
Paths: mustCompile(`\.md$`),
},
path: "docs/README.md",
want: true,
},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0