step 1: calling local:qwen3.6:35b-a3b
plan: Let me first run the test suite to identify the failures.
tool shell <- {"command":"go test ./... 2>&1","timeoutMs":120000}
tool shell failed: denied: "go test ./... 2>&1" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:a9104f8b7405c627c7874ef553dc0463adb79bbaf49869cf4b55dcaca156a638 kind tool-call:shell]
step 2: calling local:qwen3.6:35b-a3b
tool list <- {"path":"."}
tool list ok: .campaign/
.git/
.github/
.gitignore
.gitleaks.toml
.gitleaksignore
.golangci.yaml
.goreleaser.yml
.pre-commit-hooks.yaml
CONTRIBUTING.md
Dockerfile
LICENSE
Makefile
README.md
SECURITY.md
USERS.md
cmd/
config/
detect/
go.mod
go.sum
logging/
main.go
regexp/
report/
report_templates/
scripts/
sources/
testdata/
version/
[evidence record sha256:4bca23ed4710b270010d7aaf76c562d80ad586b7d2ff5cf99f2360f89024250d kind tool-call:list]
step 3: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"cat go.mod"}
tool shell ok: exit code: 0
stdout:
module github.com/zricethezav/gitleaks/v8

go 1.24.11

require (
	github.com/BobuSumisu/aho-corasick v1.0.3
	github.com/Masterminds/sprig/v3 v3.3.0
	github.com/charmbracelet/lipgloss v0.5.0
	github.com/fatih/semgroup v1.2.0
	github.com/gitleaks/go-gitdiff v0.9.1
	github.com/google/go-cmp v0.7.0
	github.com/h2non/filetype v1.1.3
	github.com/hashicorp/go-version v1.7.0
	github.com/mholt/archives v0.1.5
	github.com/rs/zerolog v1.33.0
	github.com/spf13/cobra v1.9.1
	github.com/spf13/viper v1.19.0
	github.com/stretchr/testify v1.10.0
	golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
)

require (
	dario.cat/mergo v1.0.1 // indirect
	github.com/Masterminds/goutils v1.1.1 // indirect
	github.com/Masterminds/semver/v3 v3.3.0 // indirect
	github.com/STARRY-S/zip v0.2.3 // indirect
	github.com/andybalholm/brotli v1.2.0 // indirect
	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
	github.com/bodgit/plumbing v1.3.0 // indirect
	github.com/bodgit/sevenzip v1.6.1 // indirect
	github.com/bodgit/windows v1.0.1 // indirect
	github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
	github.com/huandu/xstrings v1.5.0 // indirect
	github.com/klauspost/compress v1.18.0 // indirect
	github.com/klauspost/pgzip v1.2.6 // indirect
	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/mattn/go-runewidth v0.0.14 // indirect
	github.com/mikelolasagasti/xz v1.0.1 // indirect
	github.com/minio/minlz v1.0.1 // indirect
	github.com/mitchellh/copystructure v1.2.0 // indirect
	github.com/mitchellh/reflectwalk v1.0.2 // indirect
	github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 // indirect
	github.com/muesli/termenv v0.15.1 // indirect
	github.com/nwaples/rardecode/v2 v2.2.0 // indirect
	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
	github.com/pierrec/lz4/v4 v4.1.22 // indirect
	github.com/rivo/uniseg v0.2.0 // indirect
	github.com/sagikazarmark/locafero v0.7.0 // indirect
	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
	github.com/shopspring/decimal v1.4.0 // indirect
	github.com/sorairolake/lzip-go v0.3.8 // indirect
	github.com/sourcegraph/conc v0.3.0 // indirect
	github.com/tetratelabs/wazero v1.9.0 // indirect
	github.com/ulikunitz/xz v0.5.15 // indirect
	github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
	go.uber.org/multierr v1.11.0 // indirect
	go4.org v0.0.0-20230225012048-214862532bf5 // indirect
	golang.org/x/crypto v0.35.0 // indirect
)

require (
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/fsnotify/fsnotify v1.8.0 // indirect
	github.com/hashicorp/hcl v1.0.0 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb
	github.com/magiconair/properties v1.8.9 // indirect
	github.com/mitchellh/mapstructure v1.5.0 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/spf13/afero v1.15.0 // indirect
	github.com/spf13/cast v1.7.1 // indirect
	github.com/spf13/pflag v1.0.6 // indirect
	github.com/subosito/gotenv v1.6.0 // indirect
	github.com/wasilibs/go-re2 v1.9.0
	golang.org/x/sync v0.17.0 // indirect
	golang.org/x/sys v0.30.0 // indirect
	golang.org/x/text v0.29.0 // indirect
	gopkg.in/ini.v1 v1.67.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)
[evidence record sha256:6ead49db1cc2dd7628e81770b34783009fe9614390427370fcfd3dce792586a3 kind tool-call:shell]
step 4: calling local:qwen3.6:35b-a3b
tool read <- {"path":"Makefile"}
tool read ok: .PHONY: test test-cover failfast profile clean format build

PKG=github.com/zricethezav/gitleaks
VERSION := `git fetch --tags && git tag | sort -V | tail -1`
LDFLAGS=-ldflags "-X=github.com/zricethezav/gitleaks/v8/version.Version=$(VERSION)"
COVER=--cover --coverprofile=cover.out

test-cover:
	go test -v ./... --race $(COVER) $(PKG)
	go tool cover -html=cover.out

format:
	go fmt ./...

test: config/gitleaks.toml format
	go test -v ./... --race $(PKG)

failfast: format
	go test -failfast ./...

build: config/gitleaks.toml format
	go mod tidy
	go build $(LDFLAGS)

lint:
	golangci-lint run

clean:
	rm -rf profile
	find . -type f -name '*.got.*' -delete
	find . -type f -name '*.out' -delete

profile: build
	./scripts/profile.sh './gitleaks' '.'

config/gitleaks.toml: $(wildcard cmd/generate/config/**/*)
	go generate ./...

[evidence record sha256:8ac4a0f80726ce6ce0efe3e0d2d54fa9d83275611e99cb946c0275812368e7aa kind tool-call:read]
tool list <- {"path":"detect"}
tool list ok: baseline.go
baseline_test.go
codec/
detect.go
detect_test.go
files.go
git.go
location.go
location_test.go
reader.go
reader_test.go
utils.go
utils_test.go
[evidence record sha256:397dd20a08cd2297186922a47b85c7edde3a2dd1c2014594f35a65b4f2eb19b4 kind tool-call:list]
step 5: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go test ./detect/... 2>&1; echo \"EXIT:$?\"","timeoutMs":120000}
tool shell failed: denied: "go test ./detect/... 2>&1; echo "EXIT:$?"" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:cc286830c54a25ce4b52b9303f8a857546fd38a8f0acf6317225713926d6df41 kind tool-call:shell]
step 6: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"echo \"hello\""}
tool shell failed: denied: "echo "hello"" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:8a14364ee273bd9fdcf68d15e8a8f384541cbf8b1c14412fe28a70f73e2889c6 kind tool-call:shell]
step 7: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"ls"}
tool shell ok: exit code: 0
stdout:
CONTRIBUTING.md
Dockerfile
LICENSE
Makefile
README.md
SECURITY.md
USERS.md
cmd
config
detect
go.mod
go.sum
logging
main.go
regexp
report
report_templates
scripts
sources
testdata
version
[evidence record sha256:6234a02f5df3aeeb8be5134a6aa6ea5f69d8eed5698df137f1d722c75445be16 kind tool-call:shell]
step 8: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go version"}
tool shell failed: denied: "go version" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:2eb14eab7c087ec57b279f11cd3a94a463fb842242ddb23ebaadc26c44417ef0 kind tool-call:shell]
step 9: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect.go"}
tool read ok: package detect

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"

	ahocorasick "github.com/BobuSumisu/aho-corasick"
	"github.com/fatih/semgroup"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"golang.org/x/exp/maps"
)

const (
	gitleaksAllowSignature = "gitleaks:allow"
	// SlowWarningThreshold is the amount of time to wait before logging that a file is slow.
	// This is useful for identifying problematic files and tuning the allowlist.
	SlowWarningThreshold = 5 * time.Second
)

var (
	newLineRegexp = regexp.MustCompile("\n")
)

// Detector is the main detector struct
type Detector struct {
	// Config is the configuration for the detector
	Config config.Config

	// Redact is a flag to redact findings. This is exported
	// so users using gitleaks as a library can set this flag
	// without calling `detector.Start(cmd *cobra.Command)`
	Redact uint

	// verbose is a flag to print findings
	Verbose bool

	// MaxDecodeDepths limits how many recursive decoding passes are allowed
	MaxDecodeDepth int

	// MaxArchiveDepth limits how deep the sources will explore nested archives
	MaxArchiveDepth int

	// files larger than this will be skipped
	MaxTargetMegaBytes int

	// followSymlinks is a flag to enable scanning symlink files
	FollowSymlinks bool

	// NoColor is a flag to disable color output
	NoColor bool

	// IgnoreGitleaksAllow is a flag to ignore gitleaks:allow comments.
	IgnoreGitleaksAllow bool

	// commitMutex is to prevent concurrent access to the
	// commit map when adding commits
	commitMutex *sync.Mutex

	// commitMap is used to keep track of commits that have been scanned.
	// This is only used for logging purposes and git scans.
	commitMap map[string]bool

	// findingMutex is to prevent concurrent access to the
	// findings slice when adding findings.
	findingMutex *sync.Mutex

	// findings is a slice of report.Findings. This is the result
	// of the detector's scan which can then be used to generate a
	// report.
	findings []report.Finding

	// prefilter is a ahocorasick struct used for doing efficient string
	// matching given a set of words (keywords from the rules in the config)
	prefilter ahocorasick.Trie

	// a list of known findings that should be ignored
	baseline []report.Finding

	// path to baseline
	baselinePath string

	// gitleaksIgnore
	gitleaksIgnore map[string]struct{}

	// Sema (https://github.com/fatih/semgroup) controls the concurrency
	Sema *semgroup.Group

	// report-related settings.
	ReportPath string
	Reporter   report.Reporter

	TotalBytes atomic.Uint64
}

// Fragment is an alias for sources.Fragment for backwards compatibility
//
// Deprecated: This will be replaced with sources.Fragment in v9
type Fragment sources.Fragment

// NewDetector creates a new detector with the given config
func NewDetector(cfg config.Config) *Detector {
	return NewDetectorContext(context.Background(), cfg)
}

// NewDetectorContext is the same as NewDetector but supports passing in a
// context to use for timeouts
func NewDetectorContext(ctx context.Context, cfg config.Config) *Detector {
	return &Detector{
		commitMap:      make(map[string]bool),
		gitleaksIgnore: make(map[string]struct{}),
		findingMutex:   &sync.Mutex{},
		commitMutex:    &sync.Mutex{},
		findings:       make([]report.Finding, 0),
		Config:         cfg,
		prefilter:      *ahocorasick.NewTrieBuilder().AddStrings(maps.Keys(cfg.Keywords)).Build(),
		Sema:           semgroup.NewGroup(ctx, 40),
	}
}

// NewDetectorDefaultConfig creates a new detector with the default config
func NewDetectorDefaultConfig() (*Detector, error) {
	viper.SetConfigType("toml")
	err := viper.ReadConfig(strings.NewReader(config.DefaultConfig))
	if err != nil {
		return nil, err
	}
	var vc config.ViperConfig
	err = viper.Unmarshal(&vc)
	if err != nil {
		return nil, err
	}
	cfg, err := vc.Translate()
	if err != nil {
		return nil, err
	}
	return NewDetector(cfg), nil
}

func (d *Detector) AddGitleaksIgnore(gitleaksIgnorePath string) error {
	logging.Debug().Str("path", gitleaksIgnorePath).Msgf("found .gitleaksignore file")
	file, err := os.Open(gitleaksIgnorePath)
	if err != nil {
		return err
	}
	defer func() {
		// https://github.com/securego/gosec/issues/512
		if err := file.Close(); err != nil {
			logging.Warn().Err(err).Msgf("Error closing .gitleaksignore file")
		}
	}()

	scanner := bufio.NewScanner(file)
	replacer := strings.NewReplacer("\\", "/")
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		// Skip lines that start with a comment
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		// Normalize the path.
		// TODO: Make this a breaking change in v9.
		s := strings.Split(line, ":")
		switch len(s) {
		case 3:
			// Global fingerprint.
			// `file:rule-id:start-line`
			s[0] = replacer.Replace(s[0])
		case 4:
			// Commit fingerprint.
			// `commit:file:rule-id:start-line`
			s[1] = replacer.Replace(s[1])
		default:
			logging.Warn().Str("fingerprint", line).Msg("Invalid .gitleaksignore entry")
		}
		d.gitleaksIgnore[strings.Join(s, ":")] = struct{}{}
	}
	return nil
}

// DetectBytes scans the given bytes and returns a list of findings
func (d *Detector) DetectBytes(content []byte) []report.Finding {
	return d.DetectString(string(content))
}

// DetectString scans the given string and returns a list of findings
func (d *Detector) DetectString(content string) []report.Finding {
	return d.Detect(Fragment{
		Raw: content,
	})
}

// DetectSource scans the given source and returns a list of findings
func (d *Detector) DetectSource(ctx context.Context, source sources.Source) ([]report.Finding, error) {
	err := source.Fragments(ctx, func(fragment sources.Fragment, err error) error {
		logContext := logging.With()

		if len(fragment.FilePath) > 0 {
			logContext = logContext.Str("path", fragment.FilePath)
		}

		if len(fragment.CommitSHA) > 6 {
			logContext = logContext.Str("commit", fragment.CommitSHA[:7])
			d.addCommit(fragment.CommitSHA)
		} else if len(fragment.CommitSHA) > 0 {
			logContext = logContext.Str("commit", fragment.CommitSHA)
			d.addCommit(fragment.CommitSHA)
			logger := logContext.Logger()
			logger.Warn().Msg("commit SHAs should be >= 7 characters long")
		}

		logger := logContext.Logger()

		if err != nil {
			// Log the error and move on to the next fragment
			logger.Error().Err(err).Send()
			return nil
		}

		// both the fragment's content and path should be empty for it to be
		// considered empty at this point because of path based matches
		if len(fragment.Raw) == 0 && len(fragment.FilePath) == 0 {
			logger.Trace().Msg("skipping empty fragment")
			return nil
		}

		var timer *time.Timer
		// Only start the timer in debug mode
		if logger.GetLevel() <= zerolog.DebugLevel {
			timer = time.AfterFunc(SlowWarningThreshold, func() {
				logger.Debug().Msgf("Taking longer than %s to inspect fragment", SlowWarningThreshold.String())
			})
		}

		for _, finding := range d.DetectContext(ctx, Fragment(fragment)) {
			d.AddFinding(finding)
		}

		// Stop the timer if it was created
		if timer != nil {
			timer.Stop()
		}

		return nil
	})

	if _, isGit := source.(*sources.Git); isGit {
		logging.Info().Msgf("%d commits scanned.", len(d.commitMap))
		logging.Debug().Msg("Note: this number might be smaller than expected due to commits with no additions")
	}

	return d.Findings(), err
}

// Detect scans the given fragment and returns a list of findings
func (d *Detector) Detect(fragment Fragment) []report.Finding {
	return d.DetectContext(context.Background(), fragment)
}

// DetectContext is the same as Detect but supports passing in a
// context to use for timeouts
func (d *Detector) DetectContext(ctx context.Context, fragment Fragment) []report.Finding {
	if fragment.Bytes == nil {
		d.TotalBytes.Add(uint64(len(fragment.Raw)))
	}
	d.TotalBytes.Add(uint64(len(fragment.Bytes)))

	var (
		findings []report.Finding
		logger   = func() zerolog.Logger {
			l := logging.With().Str("path", fragment.FilePath)
			if fragment.CommitSHA != "" {
				l = l.Str("commit", fragment.CommitSHA)
			}
			return l.Logger()
		}()
	)

	// check if filepath is allowed
	if fragment.FilePath != "" {
		// is the path our config or baseline file?
		if fragment.FilePath == d.Config.Path || (d.baselinePath != "" && fragment.FilePath == d.baselinePath) {
			logging.Trace().Msg("skipping file: matches config or baseline path")
			return findings
		}
	}
	// check if commit or filepath is allowed.
	if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, d.Config.Allowlists); isAllowed {
		event.Msg("skipping file: global allowlist")
		return findings
	}

	// setup variables to handle different decoding passes
	currentRaw := fragment.Raw
	encodedSegments := []*codec.EncodedSegment{}
	currentDecodeDepth := 0
	decoder := codec.NewDecoder()

ScanLoop:
	for {
		select {
		case <-ctx.Done():
			break ScanLoop
		default:
			// build keyword map for prefiltering rules
			keywords := make(map[string]bool)
			normalizedRaw := strings.ToLower(currentRaw)
			matches := d.prefilter.MatchString(normalizedRaw)
			for _, m := range matches {
				keywords[normalizedRaw[m.Pos():int(m.Pos())+len(m.Match())]] = true
			}

			for _, rule := range d.Config.Rules {
				select {
				case <-ctx.Done():
					break ScanLoop
				default:
					if len(rule.Keywords) == 0 {
						// if no keywords are associated with the rule always scan the
						// fragment using the rule
						findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...)
						continue
					}

					// check if keywords are in the fragment
					for _, k := range rule.Keywords {
						if _, ok := keywords[strings.ToLower(k)]; ok {
							findings = append(findings, d.detectRule(fragment, currentRaw, rule, encodedSegments)...)
							break
						}
					}
				}
			}

			// increment the depth by 1 as we start our decoding pass
			currentDecodeDepth++

			// stop the loop if we've hit our max decoding depth
			if currentDecodeDepth > d.MaxDecodeDepth {
				break ScanLoop
			}

			// decode the currentRaw for the next pass
			currentRaw, encodedSegments = decoder.Decode(currentRaw, encodedSegments)

			// stop the loop when there's nothing else to decode
			if len(encodedSegments) == 0 {
				break ScanLoop
			}
		}
	}

	return filter(findings, d.Redact)
}

// detectRule scans the given fragment for the given rule and returns a list of findings
func (d *Detector) detectRule(fragment Fragment, currentRaw string, r config.Rule, encodedSegments []*codec.EncodedSegment) []report.Finding {
	var (
		findings []report.Finding
		logger   = func() zerolog.Logger {
			l := logging.With().Str("rule-id", r.RuleID).Str("path", fragment.FilePath)
			if fragment.CommitSHA != "" {
				l = l.Str("commit", fragment.CommitSHA)
			}
			return l.Logger()
		}()
	)

	if r.SkipReport && !fragment.InheritedFromFinding {
		return findings
	}

	// check if commit or file is allowed for this rule.
	if isAllowed, event := checkCommitOrPathAllowed(logger, fragment, r.Allowlists); isAllowed {
		event.Msg("skipping file: rule allowlist")
		return findings
	}

	if r.Path != nil {
		if r.Regex == nil && len(encodedSegments) == 0 {
			// Path _only_ rule
			if r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath)) {
				finding := report.Finding{
					Commit:      fragment.CommitSHA,
					RuleID:      r.RuleID,
					Description: r.Description,
					File:        fragment.FilePath,
					SymlinkFile: fragment.SymlinkFile,
					Match:       "file detected: " + fragment.FilePath,
					Tags:        r.Tags,
				}
				if fragment.CommitInfo != nil {
					finding.Author = fragment.CommitInfo.AuthorName
					finding.Date = fragment.CommitInfo.Date
					finding.Email = fragment.CommitInfo.AuthorEmail
					finding.Link = createScmLink(fragment.CommitInfo.Remote, finding)
					finding.Message = fragment.CommitInfo.Message
				}
				return append(findings, finding)
			}
		} else {
			// if path is set _and_ a regex is set, then we need to check both
			// so if the path does not match, then we should return early and not
			// consider the regex
			if !(r.Path.MatchString(fragment.FilePath) || (fragment.WindowsFilePath != "" && r.Path.MatchString(fragment.WindowsFilePath))) {
				return findings
			}
		}
	}

	// if path only rule, skip content checks
	if r.Regex == nil {
		return findings
	}

	// if flag configure and raw data size bigger then the flag
	if d.MaxTargetMegaBytes > 0 {
		rawLength := len(currentRaw) / 1_000_000
		if rawLength > d.MaxTargetMegaBytes {
			logger.Debug().
				Int("size", rawLength).
				Int("max-size", d.MaxTargetMegaBytes).
				Msg("skipping fragment: size")
			return findings
		}
	}

	matches := r.Regex.FindAllStringIndex(currentRaw, -1)
	if len(matches) == 0 {
		return findings
	}

	// TODO profile this, probably should replace with something more efficient
	newlineIndices := newLineRegexp.FindAllStringIndex(fragment.Raw, -1)

	// use currentRaw instead of fragment.Raw since this represents the current
	// decoding pass on the text
	for _, matchIndex := range r.Regex.FindAllStringIndex(currentRaw, -1) {
		// Extract secret from match
		secret := strings.Trim(currentRaw[matchIndex[0]:matchIndex[1]], "\n")

		// For any meta data from decoding
		var metaTags []string
		currentLine := ""

		// Check if the decoded portions of the segment overlap with the match
		// to see if its potentially a new match
		if len(encodedSegments) > 0 {
			segments := codec.SegmentsWithDecodedOverlap(encodedSegments, matchIndex[0], matchIndex[1])
			if len(segments) == 0 {
				// This item has already been added to a finding
				continue
			}

			matchIndex = codec.AdjustMatchIndex(segments, matchIndex)
			metaTags = append(metaTags, codec.Tags(segments)...)
			currentLine = codec.CurrentLine(segments, currentRaw)
		} else {
			// Fixes: https://github.com/gitleaks/gitleaks/issues/1352
			// removes the incorrectly following line that was detected by regex expression '\n'
			matchIndex[1] = matchIndex[0] + len(secret)
		}

		// determine location of match. Note that the location
		// in the finding will be the line/column numbers of the _match_
		// not the _secret_, which will be different if the secretGroup
		// value is set for this rule
		loc := location(newlineIndices, fragment.Raw, matchIndex)

		if matchIndex[1] > loc.endLineIndex {
			loc.endLineIndex = matchIndex[1]
		}

		finding := report.Finding{
			Commit:      fragment.CommitSHA,
			RuleID:      r.RuleID,
			Description: r.Description,
			StartLine:   fragment.StartLine + loc.startLine,
			EndLine:     fragment.StartLine + loc.endLine,
			StartColumn: loc.startColumn,
			EndColumn:   loc.endColumn,
			Line:        fragment.Raw[loc.startLineIndex:loc.endLineIndex],
			Match:       secret,
			Secret:      secret,
			File:        fragment.FilePath,
			SymlinkFile: fragment.SymlinkFile,
			Tags:        append(r.Tags, metaTags...),
		}
		if fragment.CommitInfo != nil {
			finding.Author = fragment.CommitInfo.AuthorName
			finding.Date = fragment.CommitInfo.Date
			finding.Email = fragment.CommitInfo.AuthorEmail
			finding.Link = createScmLink(fragment.CommitInfo.Remote, finding)
			finding.Message = fragment.CommitInfo.Message
		}
		if !d.IgnoreGitleaksAllow && strings.Contains(finding.Line, gitleaksAllowSignature) {
			logger.Trace().
				Str("finding", finding.Secret).
				Msg("skipping finding: 'gitleaks:allow' signature")
			continue
		}

		if currentLine == "" {
			currentLine = finding.Line
		}

		// Set the value of |secret|, if the pattern contains at least one capture group.
		// (The first element is the full match, hence we check >= 2.)
		groups := r.Regex.FindStringSubmatch(finding.Secret)
		if len(groups) >= 2 {
			if r.SecretGroup > 0 {
				if len(groups) <= r.SecretGroup {
					// Config validation should prevent this
					continue
				}
				finding.Secret = groups[r.SecretGroup]
			} else {
				// If |secretGroup| is not set, we will use the first suitable capture group.
				for _, s := range groups[1:] {
					if len(s) > 0 {
						finding.Secret = s
						break
					}
				}
			}
		}

		// check entropy
		entropy := shannonEntropy(finding.Secret)
		finding.Entropy = float32(entropy)
		if r.Entropy != 0.0 {
			// entropy is too low, skip this finding
			if entropy <= r.Entropy {
				logger.Trace().
					Str("finding", finding.Secret).
					Float32("entropy", finding.Entropy).
					Msg("skipping finding: low entropy")
				continue
			}
		}

		// check if the result matches any of the global allowlists.
		if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, d.Config.Allowlists); isAllowed {
			event.Msg("skipping finding: global allowlist")
			continue
		}

		// check if the result matches any of the rule allowlists.
		if isAllowed, event := checkFindingAllowed(logger, finding, fragment, currentLine, r.Allowlists); isAllowed {
			event.Msg("skipping finding: rule allowlist")
			continue
		}
		findings = append(findings, finding)
	}

	// Handle required rules (multi-part rules)
	if fragment.InheritedFromFinding || len(r.RequiredRules) == 0 {
		return findings
	}

	// Process required rules and create findings with auxiliary findings
	return d.processRequiredRules(fragment, currentRaw, r, encodedSegments, findings, logger)
}

// processRequiredRules handles the logic for multi-part rules with auxiliary findings
func (d *Detector) processRequiredRules(fragment Fragment, currentRaw string, r config.Rule, encodedSegments []*codec.EncodedSegment, primaryFindings []report.Finding, logger zerolog.Logger) []report.Finding {
	if len(primaryFindings) == 0 {
		logger.Debug().Msg("no primary findings to process for required rules")
		return primaryFindings
	}

	// Pre-collect all required rule findings once
	allRequiredFindings := make(map[string][]report.Finding)

	for _, requiredRule := range r.RequiredRules {
		rule, ok := d.Config.Rules[requiredRule.RuleID]
		if !ok {
			logger.Error().Str("rule-id", requiredRule.RuleID).Msg("required rule not found in config")
			continue
		}

		// Mark fragment as inherited to prevent infinite recursion
		inheritedFragment := fragment
		inheritedFragment.InheritedFromFinding = true

		// Call detectRule once for each required rule
		requiredFindings := d.detectRule(inheritedFragment, currentRaw, rule, encodedSegments)
		allRequiredFindings[requiredRule.RuleID] = requiredFindings

		logger.Debug().
			Str("rule-id", requiredRule.RuleID).
			Int("findings", len(requiredFindings)).
			Msg("collected required rule findings")
	}

	var finalFindings []report.Finding

	// Now process each primary finding against the pre-collected required findings
	for _, primaryFinding := range primaryFindings {
		var requiredFindings []*report.RequiredFinding

		for _, requiredRule := range r.RequiredRules {
			foundRequiredFindings, exists := allRequiredFindings[requiredRule.RuleID]
			if !exists {
				continue // Rule wasn't found earlier, skip
			}

			// Filter findings that are within proximity of the primary finding
			for _, requiredFinding := range foundRequiredFindings {
				if d.withinProximity(primaryFinding, requiredFinding, requiredRule) {
					req := &report.RequiredFinding{
						RuleID:      requiredFinding.RuleID,
						StartLine:   requiredFinding.StartLine,
						EndLine:     requiredFinding.EndLine,
						StartColumn: requiredFinding.StartColumn,
						EndColumn:   requiredFinding.EndColumn,
						Line:        requiredFinding.Line,
						Match:       requiredFinding.Match,
						Secret:      requiredFinding.Secret,
					}
					requiredFindings = append(requiredFindings, req)
				}
			}
		}

		// Check if we have at least one auxiliary finding for each required rule
		if len(requiredFindings) > 0 && d.hasAllRequiredRules(requiredFindings, r.RequiredRules) {
			// Create a finding with auxiliary findings
			newFinding := primaryFinding // Copy the primary finding
			newFinding.AddRequiredFindings(requiredFindings)
			finalFindings = append(finalFindings, newFinding)

			logger.Debug().
				Str("primary-rule", r.RuleID).
				Int("primary-line", primaryFinding.StartLine).
				Int("auxiliary-count", len(requiredFindings)).
				Msg("multi-part rule satisfied")
		}
	}

	return finalFindings
}

// hasAllRequiredRules checks if we have at least one auxiliary finding for each required rule
func (d *Detector) hasAllRequiredRules(auxiliaryFindings []*report.RequiredFinding, requiredRules []*config.Required) bool {
	foundRules := make(map[string]bool)
	// AuxiliaryFinding
	for _, aux := range auxiliaryFindings {
		foundRules[aux.RuleID] = true
	}

	for _, required := range requiredRules {
		if !foundRules[required.RuleID] {
			return false
		}
	}

	return true
}

func (d *Detector) withinProximity(primary, required report.Finding, requiredRule *config.Required) bool {
	// fmt.Println(requiredRule.WithinLines)
	// If neither within_lines nor within_columns is set, findings just need to be in the same fragment
	if requiredRule.WithinLines == nil && requiredRule.WithinColumns == nil {
		return true
	}

	// Check line proximity (vertical distance)
	if requiredRule.WithinLines != nil {
		lineDiff := abs(primary.StartLine - required.StartLine)
		if lineDiff > *requiredRule.WithinLines {
			return false
		}
	}

	// Check column proximity (horizontal distance)
	if requiredRule.WithinColumns != nil {
		// Use the start column of each finding for proximity calculation
		colDiff := abs(primary.StartColumn - required.StartColumn)
		if colDiff > *requiredRule.WithinColumns {
			return false
		}
	}

	return true
}

// abs returns the absolute value of an integer
func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

// AddFinding synchronously adds a finding to the findings slice
func (d *Detector) AddFinding(finding report.Finding) {
	globalFingerprint := fmt.Sprintf("%s:%s:%d", finding.File, finding.RuleID, finding.StartLine)
	if finding.Commit != "" {
		finding.Fingerprint = fmt.Sprintf("%s:%s:%s:%d", finding.Commit, finding.File, finding.RuleID, finding.StartLine)
	} else {
		finding.Fingerprint = globalFingerprint
	}

	// check if we should ignore this finding
	logger := logging.With().Str("finding", finding.Secret).Logger()
	if _, ok := d.gitleaksIgnore[globalFingerprint]; ok {
		logger.Debug().
			Str("fingerprint", globalFingerprint).
			Msg("skipping finding: global fingerprint")
		return
	} else if finding.Commit != "" {
		// Awkward nested if because I'm not sure how to chain these two conditions.
		if _, ok := d.gitleaksIgnore[finding.Fingerprint]; ok {
			logger.Debug().
				Str("fingerprint", finding.Fingerprint).
				Msgf("skipping finding: fingerprint")
			return
		}
	}

	if d.baseline != nil && !IsNew(finding, d.Redact, d.baseline) {
		logger.Debug().
			Str("fingerprint", finding.Fingerprint).
			Msgf("skipping finding: baseline")
		return
	}

	d.findingMutex.Lock()
	d.findings = append(d.findings, finding)
	if d.Verbose {
		printFinding(finding, d.NoColor)
	}
	d.findingMutex.Unlock()
}

// Findings returns the findings added to the detector
func (d *Detector) Findings() []report.Finding {
	return d.findings
}

// AddCommit synchronously adds a commit to the commit slice
func (d *Detector) addCommit(commit string) {
	d.commitMutex.Lock()
	d.commitMap[commit] = true
	d.commitMutex.Unlock()
}

// checkCommitOrPathAllowed evaluates |fragment| against all provided |allowlists|.
//
// If the match condition is "OR", only commit and path are checked.
// Otherwise, if regexes or stopwords are defined this will fail.
func checkCommitOrPathAllowed(
	logger zerolog.Logger,
	fragment Fragment,
	allowlists []*config.Allowlist,
) (bool, *zerolog.Event) {
	if fragment.FilePath == "" && fragment.CommitSHA == "" {
		return false, nil
	}

	for _, a := range allowlists {
		var (
			isAllowed        bool
			allowlistChecks  []bool
			commitAllowed, _ = a.CommitAllowed(fragment.CommitSHA)
			pathAllowed      = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath))
		)
		// If the condition is "AND" we need to check all conditions.
		if a.MatchCondition == config.AllowlistMatchAnd {
			if len(a.Commits) > 0 {
				allowlistChecks = append(allowlistChecks, commitAllowed)
			}
			if len(a.Paths) > 0 {
				allowlistChecks = append(allowlistChecks, pathAllowed)
			}
			// These will be checked later.
			if len(a.Regexes) > 0 {
				continue
			}
			if len(a.StopWords) > 0 {
				continue
			}

			isAllowed = allTrue(allowlistChecks)
		} else {
			isAllowed = commitAllowed || pathAllowed
		}
		if isAllowed {
			event := logger.Trace().Str("condition", a.MatchCondition.String())
			if commitAllowed {
				event.Bool("allowed-commit", commitAllowed)
			}
			if pathAllowed {
				event.Bool("allowed-path", pathAllowed)
			}
			return true, event
		}
	}
	return false, nil
}

// checkFindingAllowed evaluates |finding| against all provided |allowlists|.
//
// If the match condition is "OR", only regex and stopwords are run. (Commit and path should be handled separately).
// Otherwise, all conditions are checked.
//
// TODO: The method signature is awkward. I can't think of a better way to log helpful info.
func checkFindingAllowed(
	logger zerolog.Logger,
	finding report.Finding,
	fragment Fragment,
	currentLine string,
	allowlists []*config.Allowlist,
) (bool, *zerolog.Event) {
	for _, a := range allowlists {
		allowlistTarget := finding.Secret
		switch a.RegexTarget {
		case "match":
			allowlistTarget = finding.Match
		case "line":
			allowlistTarget = currentLine
		}

		var (
			checks                 []bool
			isAllowed              bool
			commitAllowed          bool
			commit                 string
			pathAllowed            bool
			regexAllowed           = a.RegexAllowed(allowlistTarget)
			containsStopword, word = a.ContainsStopWord(finding.Secret)
		)
		// If the condition is "AND" we need to check all conditions.
		if a.MatchCondition == config.AllowlistMatchAnd {
			// Determine applicable checks.
			if len(a.Commits) > 0 {
				commitAllowed, commit = a.CommitAllowed(fragment.CommitSHA)
				checks = append(checks, commitAllowed)
			}
			if len(a.Paths) > 0 {
				pathAllowed = a.PathAllowed(fragment.FilePath) || (fragment.WindowsFilePath != "" && a.PathAllowed(fragment.WindowsFilePath))
				checks = append(checks, pathAllowed)
			}
			if len(a.Regexes) > 0 {
				checks = append(checks, regexAllowed)
			}
			if len(a.StopWords) > 0 {
				checks = append(checks, containsStopword)
			}

			isAllowed = allTrue(checks)
		} else {
			isAllowed = regexAllowed || containsStopword
		}

		if isAllowed {
			event := logger.Trace().
				Str("finding", finding.Secret).
				Str("condition", a.MatchCondition.String())
			if commitAllowed {
				event.Str("allowed-commit", commit)
			}
			if pathAllowed {
				event.Bool("allowed-path", pathAllowed)
			}
			if regexAllowed {
				event.Bool("allowed-regex", regexAllowed)
			}
			if containsStopword {
				event.Str("allowed-stopword", word)
			}
			return true, event
		}
	}
	return false, nil
}

func allTrue(bools []bool) bool {
	for _, check := range bools {
		if !check {
			return false
		}
	}
	return true
}

[evidence record sha256:df5b01deb6e449b111baafe7fdb5adc6fab42d0bc492a9b18930f41afea8531f kind tool-call:read]
tool read <- {"path":"detect/detect_test.go"}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq env var, semicolon": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 21,
					EndColumn:   74,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq url": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-sensitive-url",
					Description: "Sidekiq Sensitive URL",
					File:        "tmp.sh",
					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
					Secret:      "cafeb4b3:d3adb33f",
					Entropy:     2.984234,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   58,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - our config file": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: filepath.Join(configPath, "simple.toml"),
			},
		},
		"ignore finding - doesn't match path": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.go",
			},
		},
		"detect finding - matches path,regex,entropy": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					File:        "tmp.py",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 22,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - allowlist regex": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
		},

		// Rule
		"rule - ignore path": {
			cfgName:      "valid/rule_path_only",
			baselinePath: ".baseline.json",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: ".baseline.json",
			},
		},
		"rule - detect path ": {
			cfgName: "valid/rule_path_only",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					Description: "Python Files",
					Match:       "file detected: tmp.py",
					File:        "tmp.py",
					RuleID:      "python-files-only",
					Tags:        []string{},
				},
			},
		},
		"rule - match based on entropy": {
			cfgName: "valid/rule_entropy_group",
			fragment: Fragment{
				Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "discord-api-key",
					Description: "Discord API key",
					File:        "tmp.go",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 7,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},

		// Allowlists
		"global allowlist - ignore regex": {
			cfgName: "valid/allowlist_global_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"global allowlist - detect, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw: `
const token = "mockSecret";
// const token = "changeit";`,
				FilePath: "config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "config.txt",
					Line:        "\nconst token = \"mockSecret\";",
					Match:       `token = "mockSecret"`,
					Secret:      "mockSecret",
					Entropy:     2.9219282,
					StartLine:   1,
					EndLine:     1,
					StartColumn: 8,
					EndColumn:   27,
					Tags:        []string{},
				},
			},
		},
		"global allowlist - ignore, matches all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `token := "mockSecret";`,
				FilePath: "node_modules/config.txt",
			},
		},
		"global allowlist - detect path, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `var token = "fakeSecret";`,
				FilePath: "node_modules/config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "node_modules/config.txt",
					Line:        "var token = \"fakeSecret\";",
					Match:       `token = "fakeSecret"`,
					Secret:      "fakeSecret",
					Entropy:     2.8464394,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 5,
					EndColumn:   24,
					Tags:        []string{},
				},
			},
		},
		"allowlist - ignore commit": {
			cfgName: "valid/allowlist_rule_commit",
			fragment: Fragment{
				Raw:       `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath:  "tmp.go",
				CommitSHA: "allowthiscommit",
			},
		},
		"allowlist - ignore path": {
			cfgName: "valid/allowlist_rule_path",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"allowlist - ignore path when extending": {
			cfgName: "valid/allowlist_rule_extend_default",
			fragment: Fragment{
				Raw:      `token = "aebfab88-7596-481d-82e8-c60c8f7de0c0"`,
				FilePath: "path/to/your/problematic/file.js",
			},
		},
		"allowlist - ignore regex": {
			cfgName: "valid/allowlist_rule_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"fragment level composite": {
			cfgName: "composite",
			fragment: Fragment{
				Raw: multili,
			},
			expectedFindings: []report.Finding{
				{
					Description: "Primary rule",
					RuleID:      "primary-rule",
					StartLine:   5,
					EndLine:     5,
					StartColumn: 5,
					EndColumn:   26,
					Line:        "\n\t\t\tpassword = \"secret123\"",
					Match:       `password = "secret123"`,
					Secret:      "secret123",
					Entropy:     2.9477028846740723,
					Tags:        []string{},
				},
			},
			expectedAuxOutput: "Required:    username-rule:1:admin\n",
		},
		// Decoding
		"detect encoded": {
			cfgName: "encoded",
			fragment: Fragment{
				Raw:      encodedTestValues,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{ // Plain text key captured by normal rule
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					RuleID:      "private-key",
					Tags:        []string{"key", "private"},
					StartLine:   2,
					EndLine:     5,
					StartColumn: 2,
					EndColumn:   26,
					Entropy:     5.350665,
				},
				{ // Encoded key captured by custom b64 regex rule
					Description: "Private Key",
					Secret:      "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					Match:       "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "b64-encoded-private-key",
					Tags:        []string{"key", "private"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.3861146,
				},
				{ // Encoded key captured by plain text rule using the decoder
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "private-key",
					Tags:        []string{"key", "private", "decoded:base64", "decode-depth:1"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.350665,
				},
				{ // Encoded Small secret at the end to make sure it's picked up by the decoding
					Description: "Small Secret",
					Secret:      "small-secret",
					Match:       "small-secret",
					File:        "tmp.go",
					Line:        "\nc21hbGwtc2VjcmV0",
					RuleID:      "small-secret",
					Tags:        []string{"small", "secret", "decoded:base64", "decode-depth:1"},
					StartLine:   15,
					EndLine:     15,
					StartColumn: 2,
					EndColumn:   17,
					Entropy:     3.0849626,
				},
				{ // Secret where the decoded match goes outside the encoded value
					Description: "Overlapping",
					Secret:      "decoded-secret-value00",
					Match:       "secret=decoded-secret-value00",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:base64", "decode-depth:1"},
					StartLine:   18,
					EndLine:     18,
					StartColumn: 2,
					EndColumn:   40,
					Entropy:     3.4428623,
				},
				{ // This just confirms that with no allowlist the pattern is detected (i.e. the regex is good)
					Description: "Make sure this would be detected with no allowlist",
					Secret:      "lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw",
					Match:       "password=\"lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw\"",
					File:        "tmp.go",
					Line:        "\npassword=\"bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==\"",
					RuleID:      "decoded-password-dont-ignore",
					Tags:        []string{"decode-ignore", "decoded:base64", "decode-depth:1"},
					StartLine:   23,
					EndLine:     23,
					StartColumn: 2,
					EndColumn:   68,
					Entropy:     4.5841837,
				},
				{ // Hex encoded data check
					Description: "Overlapping",
					Secret:      "decoded-secret-valuevHEX",
					Match:       "secret=decoded-secret-valuevHEX",
					File:        "tmp.go",
					Line:        "\nsecret=6465636F6465642D7365637265742D76616C756576484558",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:hex", "decode-depth:1"},
					StartLine:   26,
					EndLine:     26,
					StartColumn: 2,
					EndColumn:   56,
					Entropy:     3.6531072,
				},
				{ // handle partial encoded percent data
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev2",
					Match:       "secret=decoded-secret-valuev2",
					File:        "tmp.go",
					Line:        "\nsecret=decoded-%73%65%63%72%65%74-valuev2",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decode-depth:1"},
					StartLine:   30,
					EndLine:     30,
					StartColumn: 2,
					EndColumn:   42,
					Entropy:     3.4428623,
				},
				{ // handle partial encoded percent data
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev3",
					Match:       "secret=decoded-secret-valuev3",
					File:        "tmp.go",
					Line:        "\nsecret=%64%65coded-%73%65%63%72%65%74-valuev3",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decode-depth:1"},
					StartLine:   32,
					EndLine:     32,
					StartColumn: 2,
					EndColumn:   46,
					Entropy:     3.4428623,
				},
				{ // Encoded AWS config with a access key id inside a JWT
					Description: "AWS IAM Unique Identifier",
					Secret:      "ASIAIOSFODNN7LXM10JI",
					Match:       " ASIAIOSFODNN7LXM10JI",
					File:        "tmp.go",
					Line:        "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
					RuleID:      "aws-iam-unique-identifier",
					Tags:        []string{"aws", "identifier", "decoded:base64", "decode-depth:2"},
					StartLine:   11,
					EndLine:     11,
					StartColumn: 39,
					EndColumn:   344,
					Entropy:     3.6841838,
				},
				{ // Encoded AWS config with a secret access key inside a JWT
					Description: "AWS Secret Access Key",
					Secret:      "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
					Match:       "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
					File:        "tmp.go",
					Line:        "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
					RuleID:      "aws-secret-access-key",
					Tags:        []string{"aws", "secret", "decoded:base64", "decode-depth:2"},
					StartLine:   11,
					EndLine:     11,
					StartColumn: 39,
					EndColumn:   344,
					Entropy:     4.721928,
				},
				{ // Secret where the decoded match goes outside the encoded value and then encoded again
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nc2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:base64", "decode-depth:2"},
					StartLine:   20,
					EndLine:     20,
					StartColumn: 2,
					EndColumn:   49,
					Entropy:     3.3037016,
				},
				{ // handle encodings that touch eachother
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev5",
					Match:       "secret=decoded-secret-valuev5",
					File:        "tmp.go",
					Line:        "\nsecret%3d6465636F6465642D7365637265742D76616C75657635",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:hex", "decode-depth:2"},
					StartLine:   40,
					EndLine:     40,
					StartColumn: 2,
					EndColumn:   54,
					Entropy:     3.4428623,
				},
				{ // handle partial encoded percent data465642D7365637265742D76616C75657635
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev4",
					Match:       "secret=decoded-secret-valuev4",
					File:        "tmp.go",
					Line:        "\nc2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:3"},
					StartLine:   38,
					EndLine:     38,
					StartColumn: 2,
					EndColumn:   55,
					Entropy:     3.4428623,
				},
				{ // multiple percent encodings in a single layer base64
					Description: "Overlapping",
					Secret:      "decoded-secret-valuex86",
					Match:       "secret=decoded-secret-valuex86",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"},
					StartLine:   42,
					EndLine:     42,
					StartColumn: 2,
					EndColumn:   44,
					Entropy:     3.6381476,
				},
				{ // base64 encoded partially percent encoded value
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"},
					StartLine:   44,
					EndLine:     44,
					StartColumn: 2,
					EndColumn:   52,
					Entropy:     3.3037016,
				},
				{ // one of the lines above that went through... a lot
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nLook at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:hex", "decoded:base64", "decode-depth:7"},
					StartLine:   47,
					EndLine:     47,
					StartColumn: 22,
					EndColumn:   177,
					Entropy:     3.3037016,
				},
				{ // Multi percent encode two random characters close to the bounds of the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:5"},
					StartLine:   50,
					EndLine:     50,
					StartColumn: 2,
					EndColumn:   300,
					Entropy:     3.3037016,
				},
				{ // The similar to the above but also touching the edge of the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"},
					StartLine:   52,
					EndLine:     52,
					StartColumn: 2,
					EndColumn:   86,
					Entropy:     3.3037016,
				},
				{ // The similar to the above but also touching and overlapping the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"},
					StartLine:   54,
					EndLine:     54,
					StartColumn: 2,
					EndColumn:   88,
					Entropy:     3.3037016,
				},
			},
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			viper.Reset()
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)
			cfg, err := vc.Translate()
			cfg.Path = filepath.Join(configPath, tt.cfgName+".toml")
			assert.Equal(t, tt.wantError, err)
			d := NewDetector(cfg)
			d.MaxDecodeDepth = maxDecodeDepth
			d.baselinePath = tt.baselinePath

			findings := d.Detect(tt.fragment)

			compare(t, findings, tt.expectedFindings)

			// extremely goofy way to test auxiliary findings
			// capture stdout and print that sonabitch
			// TODO
			if tt.expectedAuxOutput != "" {
				capturedOutput := captureStdout(func() {
					for _, finding := range findings {
						finding.PrintRequiredFindings()
					}
				})

				// Clean up the output for comparison (remove ANSI color codes)
				cleanOutput := stripANSI(capturedOutput)
				expectedClean := stripANSI(tt.expectedAuxOutput)

				assert.Equal(t, expectedClean, cleanOutput, "Auxiliary output should match")
			}

		})
	}
}

func stripANSI(s string) string {
	ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
	return ansiRegex.ReplaceAllString(s, "")
}

func captureStdout(f func()) string {
	oldStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	f()

	w.Close()
	os.Stdout = oldStdout

	var buf bytes.Buffer
	io.Copy(&buf, r)
	return buf.String()
}

// TestFromGit tests the FromGit function
func TestFromGit(t *testing.T) {
	// TODO: Fix this test on windows.
	if runtime.GOOS == "windows" {
		t.Skipf("TODO: this fails on Windows: [git] fatal: bad object refs/remotes/origin/main?")
		return
	}
	tests := []struct {
		cfgName          string
		source           string
		logOpts          string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "small"),
			cfgName: "simple", // the remote url is `git@github.com:gitleaks/test.git`
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 19,
					EndColumn:   38,
					Line:        "\n    awsToken := \"AKIALALEMEL33243OLIA\"",
					Secret:      "AKIALALEMEL33243OLIA",
					Match:       "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					File:        "main.go",
					Date:        "2021-11-02T23:37:53Z",
					Commit:      "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
					Author:      "Zachary Rice",
					Email:       "zricer@protonmail.com",
					Message:     "Accidentally add a secret",
					Tags:        []string{"key", "AWS"},
					Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/1b6da43b82b22e4eaa10bcf8ee591e91abbfc587/main.go#L20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   9,
					EndLine:     9,
					StartColumn: 17,
					EndColumn:   36,
					Secret:      "AKIALALEMEL33243OLIA",
					Match:       "AKIALALEMEL33243OLIA",
					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
					File:        "foo/foo.go",
					Date:        "2021-11-02T23:48:06Z",
					Commit:      "491504d5a31946ce75e22554cc34203d8e5ff3ca",
					Author:      "Zach Rice",
					Email:       "zricer@protonmail.com",
					Message:     "adding foo package with secret",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
					Link:        "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "small"),
			logOpts: "--all foo...",
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   9,
					EndLine:     9,
					StartColumn: 17,
					EndColumn:   36,
					Secret:      "AKIALALEMEL33243OLIA",
					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Date:        "2021-11-02T23:48:06Z",
					File:        "foo/foo.go",
					Commit:      "491504d5a31946ce75e22554cc34203d8e5ff3ca",
					Author:      "Zach Rice",
					Email:       "zricer@protonmail.com",
					Message:     "adding foo package with secret",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
					Link:        "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "archives"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "main.go.zst",
					Commit:      "db8789716fc664dbce0ed2d492570e92abf717a5",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:10:39Z",
					Message:     "Add main.go.zst",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "db8789716fc664dbce0ed2d492570e92abf717a5:main.go.zst:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/db8789716fc664dbce0ed2d492570e92abf717a5/main.go.zst#L20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.zip!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.zip!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.7z!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.7z!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.zst!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.zst!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.xz",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.xz:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.zst",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.zst:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.gz",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.gz:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.xz!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.xz!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
			},
		},
	}

	moveDotGit(t, "dotGit", ".git")
	defer moveDotGit(t, ".git", "dotGit")

	for _, tt := range tests {
		t.Run(strings.Join([]string{tt.cfgName, tt.source, tt.logOpts}, "/"), func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName("simple")
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)
			cfg, err := vc.Translate()
			require.NoError(t, err)
			detector := NewDetector(cfg)
			detector.MaxArchiveDepth = 8

			var ignorePath string
			info, err := os.Stat(tt.source)
			require.NoError(t, err)

			if info.IsDir() {
				ignorePath = filepath.Join(tt.source, ".gitleaksignore")
			} else {
				ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
			}
			err = detector.AddGitleaksIgnore(ignorePath)
			require.NoError(t, err)

			gitCmd, err := sources.NewGitLogCmd(tt.source, tt.logOpts)
			require.NoError(t, err)

			remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
			findings, err := detector.DetectGit(gitCmd, remote)
			require.NoError(t, err)

			for _, f := range findings {
				f.Match = "" // remove lines cause copying and pasting them has some wack formatting
			}
			assert.ElementsMatch(t, tt.expectedFindings, findings)
		})
	}
}

func TestFromGitStaged(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		logOpts          string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "staged"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   7,
					EndLine:     7,
					StartColumn: 18,
					EndColumn:   37,
					Line:        "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "api/api.go",
					SymlinkFile: "",
					Commit:      "",
					Entropy:     3.0841837,
					Author:      "",
					Email:       "",
					Date:        "0001-01-01T00:00:00Z",
					Message:     "",
					Tags: []string{
						"key",
						"AWS",
					},
					Fingerprint: "api/api.go:aws-access-key:7",
					Link:        "",
				},
			},
		},
	}

	moveDotGit(t, "dotGit", ".git")
	defer moveDotGit(t, ".git", "dotGit")
	for _, tt := range tests {

		viper.AddConfigPath(configPath)
		viper.SetConfigName("simple")
		viper.SetConfigType("toml")
		err := viper.ReadInConfig()
		require.NoError(t, err)

		var vc config.ViperConfig
		err = viper.Unmarshal(&vc)
		require.NoError(t, err)
		cfg, err := vc.Translate()
		require.NoError(t, err)
		detector := NewDetector(cfg)
		err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore"))
		require.NoError(t, err)
		gitCmd, err := sources.NewGitDiffCmd(tt.source, true)
		require.NoError(t, err)
		remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
		findings, err := detector.DetectGit(gitCmd, remote)
		require.NoError(t, err)

		for _, f := range findings {
			f.Match = "" // remove lines cause copying and pasting them has some wack formatting
		}
		assert.ElementsMatch(t, tt.expectedFindings, findings)
	}
}

// TestFromFiles tests the FromFiles function
func TestFromFiles(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "nogit"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/repos/nogit/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "nogit", "main.go"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/repos/nogit/main.go",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:           filepath.Join(repoBasePath, "nogit", "api.go"),
			cfgName:          "simple",
			expectedFindings: []report.Finding{},
		},
		{
			source:  filepath.Join(repoBasePath, "nogit", ".env.prod"),
			cfgName: "generic",
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					StartLine:   4,
					EndLine:     4,
					StartColumn: 5,
					EndColumn:   35,
					Line:        "\nDB_PASSWORD=8ae31cacf141669ddfb5da",
					Match:       "PASSWORD=8ae31cacf141669ddfb5da",
					Secret:      "8ae31cacf141669ddfb5da",
					File:        "../testdata/repos/nogit/.env.prod",
					Tags:        []string{},
					Entropy:     3.5383105,
					Fingerprint: "../testdata/repos/nogit/.env.prod:generic-api-key:4",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			cfg, _ := vc.Translate()
			detector := NewDetector(cfg)

			info, err := os.Stat(tt.source)
			require.NoError(t, err)

			var ignorePath string
			if info.IsDir() {
				ignorePath = filepath.Join(tt.source, ".gitleaksignore")
			} else {
				ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
			}
			err = detector.AddGitleaksIgnore(ignorePath)
			require.NoError(t, err)

			detector.FollowSymlinks = true
			paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
			require.NoError(t, err)

			findings, err := detector.DetectFiles(paths)
			require.NoError(t, err)

			// TODO: Temporary mitigation.
			// https://github.com/gitleaks/gitleaks/issues/1641
			normalizedFindings := make([]report.Finding, len(findings))
			for i, f := range findings {
				if strings.HasSuffix(f.Line, "\r") {
					f.Line = strings.ReplaceAll(f.Line, "\r", "")
				}
				if strings.HasSuffix(f.Match, "\r") {
					f.EndColumn = f.EndColumn - 1
					f.Match = strings.ReplaceAll(f.Match, "\r", "")
				}
				normalizedFindings[i] = f
			}
			assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
		})
	}
}

func TestDetectWithArchives(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		expireContext    bool
		expectedFindings []report.Finding
	}{
		{
			source:           filepath.Join(archivesBasePath, "this-path-does-not-exist"),
			cfgName:          "archives",
			expectedFindings: []report.Finding{},
		},
		{
			source:  filepath.Join(archivesBasePath, "files"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.gz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.gz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.xz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.xz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.zst",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.zst:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.7z"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.7z!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.7z!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.7z!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.7z!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.tar"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.tar.xz"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.xz!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar.xz!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.xz!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar.xz!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.tar.zst"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.zst!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar.zst!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.zst!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "..
[truncated at 64000 of 95196 bytes]
[evidence record sha256:f1498900a064b7611051203faf2521fbab4427009ba872f5a522923d8b76e349 kind tool-call:read]
step 10: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect_test.go","maxBytes":95196}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq env var, semicolon": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 21,
					EndColumn:   74,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq url": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-sensitive-url",
					Description: "Sidekiq Sensitive URL",
					File:        "tmp.sh",
					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
					Secret:      "cafeb4b3:d3adb33f",
					Entropy:     2.984234,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   58,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - our config file": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: filepath.Join(configPath, "simple.toml"),
			},
		},
		"ignore finding - doesn't match path": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.go",
			},
		},
		"detect finding - matches path,regex,entropy": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					File:        "tmp.py",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 22,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - allowlist regex": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
		},

		// Rule
		"rule - ignore path": {
			cfgName:      "valid/rule_path_only",
			baselinePath: ".baseline.json",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: ".baseline.json",
			},
		},
		"rule - detect path ": {
			cfgName: "valid/rule_path_only",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					Description: "Python Files",
					Match:       "file detected: tmp.py",
					File:        "tmp.py",
					RuleID:      "python-files-only",
					Tags:        []string{},
				},
			},
		},
		"rule - match based on entropy": {
			cfgName: "valid/rule_entropy_group",
			fragment: Fragment{
				Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "discord-api-key",
					Description: "Discord API key",
					File:        "tmp.go",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 7,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},

		// Allowlists
		"global allowlist - ignore regex": {
			cfgName: "valid/allowlist_global_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"global allowlist - detect, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw: `
const token = "mockSecret";
// const token = "changeit";`,
				FilePath: "config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "config.txt",
					Line:        "\nconst token = \"mockSecret\";",
					Match:       `token = "mockSecret"`,
					Secret:      "mockSecret",
					Entropy:     2.9219282,
					StartLine:   1,
					EndLine:     1,
					StartColumn: 8,
					EndColumn:   27,
					Tags:        []string{},
				},
			},
		},
		"global allowlist - ignore, matches all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `token := "mockSecret";`,
				FilePath: "node_modules/config.txt",
			},
		},
		"global allowlist - detect path, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `var token = "fakeSecret";`,
				FilePath: "node_modules/config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "node_modules/config.txt",
					Line:        "var token = \"fakeSecret\";",
					Match:       `token = "fakeSecret"`,
					Secret:      "fakeSecret",
					Entropy:     2.8464394,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 5,
					EndColumn:   24,
					Tags:        []string{},
				},
			},
		},
		"allowlist - ignore commit": {
			cfgName: "valid/allowlist_rule_commit",
			fragment: Fragment{
				Raw:       `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath:  "tmp.go",
				CommitSHA: "allowthiscommit",
			},
		},
		"allowlist - ignore path": {
			cfgName: "valid/allowlist_rule_path",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"allowlist - ignore path when extending": {
			cfgName: "valid/allowlist_rule_extend_default",
			fragment: Fragment{
				Raw:      `token = "aebfab88-7596-481d-82e8-c60c8f7de0c0"`,
				FilePath: "path/to/your/problematic/file.js",
			},
		},
		"allowlist - ignore regex": {
			cfgName: "valid/allowlist_rule_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"fragment level composite": {
			cfgName: "composite",
			fragment: Fragment{
				Raw: multili,
			},
			expectedFindings: []report.Finding{
				{
					Description: "Primary rule",
					RuleID:      "primary-rule",
					StartLine:   5,
					EndLine:     5,
					StartColumn: 5,
					EndColumn:   26,
					Line:        "\n\t\t\tpassword = \"secret123\"",
					Match:       `password = "secret123"`,
					Secret:      "secret123",
					Entropy:     2.9477028846740723,
					Tags:        []string{},
				},
			},
			expectedAuxOutput: "Required:    username-rule:1:admin\n",
		},
		// Decoding
		"detect encoded": {
			cfgName: "encoded",
			fragment: Fragment{
				Raw:      encodedTestValues,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{ // Plain text key captured by normal rule
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					RuleID:      "private-key",
					Tags:        []string{"key", "private"},
					StartLine:   2,
					EndLine:     5,
					StartColumn: 2,
					EndColumn:   26,
					Entropy:     5.350665,
				},
				{ // Encoded key captured by custom b64 regex rule
					Description: "Private Key",
					Secret:      "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					Match:       "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "b64-encoded-private-key",
					Tags:        []string{"key", "private"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.3861146,
				},
				{ // Encoded key captured by plain text rule using the decoder
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "private-key",
					Tags:        []string{"key", "private", "decoded:base64", "decode-depth:1"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.350665,
				},
				{ // Encoded Small secret at the end to make sure it's picked up by the decoding
					Description: "Small Secret",
					Secret:      "small-secret",
					Match:       "small-secret",
					File:        "tmp.go",
					Line:        "\nc21hbGwtc2VjcmV0",
					RuleID:      "small-secret",
					Tags:        []string{"small", "secret", "decoded:base64", "decode-depth:1"},
					StartLine:   15,
					EndLine:     15,
					StartColumn: 2,
					EndColumn:   17,
					Entropy:     3.0849626,
				},
				{ // Secret where the decoded match goes outside the encoded value
					Description: "Overlapping",
					Secret:      "decoded-secret-value00",
					Match:       "secret=decoded-secret-value00",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:base64", "decode-depth:1"},
					StartLine:   18,
					EndLine:     18,
					StartColumn: 2,
					EndColumn:   40,
					Entropy:     3.4428623,
				},
				{ // This just confirms that with no allowlist the pattern is detected (i.e. the regex is good)
					Description: "Make sure this would be detected with no allowlist",
					Secret:      "lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw",
					Match:       "password=\"lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw\"",
					File:        "tmp.go",
					Line:        "\npassword=\"bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==\"",
					RuleID:      "decoded-password-dont-ignore",
					Tags:        []string{"decode-ignore", "decoded:base64", "decode-depth:1"},
					StartLine:   23,
					EndLine:     23,
					StartColumn: 2,
					EndColumn:   68,
					Entropy:     4.5841837,
				},
				{ // Hex encoded data check
					Description: "Overlapping",
					Secret:      "decoded-secret-valuevHEX",
					Match:       "secret=decoded-secret-valuevHEX",
					File:        "tmp.go",
					Line:        "\nsecret=6465636F6465642D7365637265742D76616C756576484558",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:hex", "decode-depth:1"},
					StartLine:   26,
					EndLine:     26,
					StartColumn: 2,
					EndColumn:   56,
					Entropy:     3.6531072,
				},
				{ // handle partial encoded percent data
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev2",
					Match:       "secret=decoded-secret-valuev2",
					File:        "tmp.go",
					Line:        "\nsecret=decoded-%73%65%63%72%65%74-valuev2",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decode-depth:1"},
					StartLine:   30,
					EndLine:     30,
					StartColumn: 2,
					EndColumn:   42,
					Entropy:     3.4428623,
				},
				{ // handle partial encoded percent data
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev3",
					Match:       "secret=decoded-secret-valuev3",
					File:        "tmp.go",
					Line:        "\nsecret=%64%65coded-%73%65%63%72%65%74-valuev3",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decode-depth:1"},
					StartLine:   32,
					EndLine:     32,
					StartColumn: 2,
					EndColumn:   46,
					Entropy:     3.4428623,
				},
				{ // Encoded AWS config with a access key id inside a JWT
					Description: "AWS IAM Unique Identifier",
					Secret:      "ASIAIOSFODNN7LXM10JI",
					Match:       " ASIAIOSFODNN7LXM10JI",
					File:        "tmp.go",
					Line:        "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
					RuleID:      "aws-iam-unique-identifier",
					Tags:        []string{"aws", "identifier", "decoded:base64", "decode-depth:2"},
					StartLine:   11,
					EndLine:     11,
					StartColumn: 39,
					EndColumn:   344,
					Entropy:     3.6841838,
				},
				{ // Encoded AWS config with a secret access key inside a JWT
					Description: "AWS Secret Access Key",
					Secret:      "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
					Match:       "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
					File:        "tmp.go",
					Line:        "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
					RuleID:      "aws-secret-access-key",
					Tags:        []string{"aws", "secret", "decoded:base64", "decode-depth:2"},
					StartLine:   11,
					EndLine:     11,
					StartColumn: 39,
					EndColumn:   344,
					Entropy:     4.721928,
				},
				{ // Secret where the decoded match goes outside the encoded value and then encoded again
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nc2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:base64", "decode-depth:2"},
					StartLine:   20,
					EndLine:     20,
					StartColumn: 2,
					EndColumn:   49,
					Entropy:     3.3037016,
				},
				{ // handle encodings that touch eachother
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev5",
					Match:       "secret=decoded-secret-valuev5",
					File:        "tmp.go",
					Line:        "\nsecret%3d6465636F6465642D7365637265742D76616C75657635",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:hex", "decode-depth:2"},
					StartLine:   40,
					EndLine:     40,
					StartColumn: 2,
					EndColumn:   54,
					Entropy:     3.4428623,
				},
				{ // handle partial encoded percent data465642D7365637265742D76616C75657635
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev4",
					Match:       "secret=decoded-secret-valuev4",
					File:        "tmp.go",
					Line:        "\nc2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:3"},
					StartLine:   38,
					EndLine:     38,
					StartColumn: 2,
					EndColumn:   55,
					Entropy:     3.4428623,
				},
				{ // multiple percent encodings in a single layer base64
					Description: "Overlapping",
					Secret:      "decoded-secret-valuex86",
					Match:       "secret=decoded-secret-valuex86",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"},
					StartLine:   42,
					EndLine:     42,
					StartColumn: 2,
					EndColumn:   44,
					Entropy:     3.6381476,
				},
				{ // base64 encoded partially percent encoded value
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"},
					StartLine:   44,
					EndLine:     44,
					StartColumn: 2,
					EndColumn:   52,
					Entropy:     3.3037016,
				},
				{ // one of the lines above that went through... a lot
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nLook at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:hex", "decoded:base64", "decode-depth:7"},
					StartLine:   47,
					EndLine:     47,
					StartColumn: 22,
					EndColumn:   177,
					Entropy:     3.3037016,
				},
				{ // Multi percent encode two random characters close to the bounds of the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:5"},
					StartLine:   50,
					EndLine:     50,
					StartColumn: 2,
					EndColumn:   300,
					Entropy:     3.3037016,
				},
				{ // The similar to the above but also touching the edge of the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"},
					StartLine:   52,
					EndLine:     52,
					StartColumn: 2,
					EndColumn:   86,
					Entropy:     3.3037016,
				},
				{ // The similar to the above but also touching and overlapping the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"},
					StartLine:   54,
					EndLine:     54,
					StartColumn: 2,
					EndColumn:   88,
					Entropy:     3.3037016,
				},
			},
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			viper.Reset()
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)
			cfg, err := vc.Translate()
			cfg.Path = filepath.Join(configPath, tt.cfgName+".toml")
			assert.Equal(t, tt.wantError, err)
			d := NewDetector(cfg)
			d.MaxDecodeDepth = maxDecodeDepth
			d.baselinePath = tt.baselinePath

			findings := d.Detect(tt.fragment)

			compare(t, findings, tt.expectedFindings)

			// extremely goofy way to test auxiliary findings
			// capture stdout and print that sonabitch
			// TODO
			if tt.expectedAuxOutput != "" {
				capturedOutput := captureStdout(func() {
					for _, finding := range findings {
						finding.PrintRequiredFindings()
					}
				})

				// Clean up the output for comparison (remove ANSI color codes)
				cleanOutput := stripANSI(capturedOutput)
				expectedClean := stripANSI(tt.expectedAuxOutput)

				assert.Equal(t, expectedClean, cleanOutput, "Auxiliary output should match")
			}

		})
	}
}

func stripANSI(s string) string {
	ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
	return ansiRegex.ReplaceAllString(s, "")
}

func captureStdout(f func()) string {
	oldStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	f()

	w.Close()
	os.Stdout = oldStdout

	var buf bytes.Buffer
	io.Copy(&buf, r)
	return buf.String()
}

// TestFromGit tests the FromGit function
func TestFromGit(t *testing.T) {
	// TODO: Fix this test on windows.
	if runtime.GOOS == "windows" {
		t.Skipf("TODO: this fails on Windows: [git] fatal: bad object refs/remotes/origin/main?")
		return
	}
	tests := []struct {
		cfgName          string
		source           string
		logOpts          string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "small"),
			cfgName: "simple", // the remote url is `git@github.com:gitleaks/test.git`
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 19,
					EndColumn:   38,
					Line:        "\n    awsToken := \"AKIALALEMEL33243OLIA\"",
					Secret:      "AKIALALEMEL33243OLIA",
					Match:       "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					File:        "main.go",
					Date:        "2021-11-02T23:37:53Z",
					Commit:      "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
					Author:      "Zachary Rice",
					Email:       "zricer@protonmail.com",
					Message:     "Accidentally add a secret",
					Tags:        []string{"key", "AWS"},
					Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/1b6da43b82b22e4eaa10bcf8ee591e91abbfc587/main.go#L20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   9,
					EndLine:     9,
					StartColumn: 17,
					EndColumn:   36,
					Secret:      "AKIALALEMEL33243OLIA",
					Match:       "AKIALALEMEL33243OLIA",
					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
					File:        "foo/foo.go",
					Date:        "2021-11-02T23:48:06Z",
					Commit:      "491504d5a31946ce75e22554cc34203d8e5ff3ca",
					Author:      "Zach Rice",
					Email:       "zricer@protonmail.com",
					Message:     "adding foo package with secret",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
					Link:        "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "small"),
			logOpts: "--all foo...",
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   9,
					EndLine:     9,
					StartColumn: 17,
					EndColumn:   36,
					Secret:      "AKIALALEMEL33243OLIA",
					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Date:        "2021-11-02T23:48:06Z",
					File:        "foo/foo.go",
					Commit:      "491504d5a31946ce75e22554cc34203d8e5ff3ca",
					Author:      "Zach Rice",
					Email:       "zricer@protonmail.com",
					Message:     "adding foo package with secret",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
					Link:        "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "archives"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "main.go.zst",
					Commit:      "db8789716fc664dbce0ed2d492570e92abf717a5",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:10:39Z",
					Message:     "Add main.go.zst",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "db8789716fc664dbce0ed2d492570e92abf717a5:main.go.zst:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/db8789716fc664dbce0ed2d492570e92abf717a5/main.go.zst#L20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.zip!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.zip!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.7z!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.7z!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.zst!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.zst!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.xz",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.xz:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.zst",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.zst:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.gz",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.gz:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.xz!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.xz!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
			},
		},
	}

	moveDotGit(t, "dotGit", ".git")
	defer moveDotGit(t, ".git", "dotGit")

	for _, tt := range tests {
		t.Run(strings.Join([]string{tt.cfgName, tt.source, tt.logOpts}, "/"), func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName("simple")
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)
			cfg, err := vc.Translate()
			require.NoError(t, err)
			detector := NewDetector(cfg)
			detector.MaxArchiveDepth = 8

			var ignorePath string
			info, err := os.Stat(tt.source)
			require.NoError(t, err)

			if info.IsDir() {
				ignorePath = filepath.Join(tt.source, ".gitleaksignore")
			} else {
				ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
			}
			err = detector.AddGitleaksIgnore(ignorePath)
			require.NoError(t, err)

			gitCmd, err := sources.NewGitLogCmd(tt.source, tt.logOpts)
			require.NoError(t, err)

			remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
			findings, err := detector.DetectGit(gitCmd, remote)
			require.NoError(t, err)

			for _, f := range findings {
				f.Match = "" // remove lines cause copying and pasting them has some wack formatting
			}
			assert.ElementsMatch(t, tt.expectedFindings, findings)
		})
	}
}

func TestFromGitStaged(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		logOpts          string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "staged"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   7,
					EndLine:     7,
					StartColumn: 18,
					EndColumn:   37,
					Line:        "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "api/api.go",
					SymlinkFile: "",
					Commit:      "",
					Entropy:     3.0841837,
					Author:      "",
					Email:       "",
					Date:        "0001-01-01T00:00:00Z",
					Message:     "",
					Tags: []string{
						"key",
						"AWS",
					},
					Fingerprint: "api/api.go:aws-access-key:7",
					Link:        "",
				},
			},
		},
	}

	moveDotGit(t, "dotGit", ".git")
	defer moveDotGit(t, ".git", "dotGit")
	for _, tt := range tests {

		viper.AddConfigPath(configPath)
		viper.SetConfigName("simple")
		viper.SetConfigType("toml")
		err := viper.ReadInConfig()
		require.NoError(t, err)

		var vc config.ViperConfig
		err = viper.Unmarshal(&vc)
		require.NoError(t, err)
		cfg, err := vc.Translate()
		require.NoError(t, err)
		detector := NewDetector(cfg)
		err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore"))
		require.NoError(t, err)
		gitCmd, err := sources.NewGitDiffCmd(tt.source, true)
		require.NoError(t, err)
		remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
		findings, err := detector.DetectGit(gitCmd, remote)
		require.NoError(t, err)

		for _, f := range findings {
			f.Match = "" // remove lines cause copying and pasting them has some wack formatting
		}
		assert.ElementsMatch(t, tt.expectedFindings, findings)
	}
}

// TestFromFiles tests the FromFiles function
func TestFromFiles(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "nogit"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/repos/nogit/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "nogit", "main.go"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/repos/nogit/main.go",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:           filepath.Join(repoBasePath, "nogit", "api.go"),
			cfgName:          "simple",
			expectedFindings: []report.Finding{},
		},
		{
			source:  filepath.Join(repoBasePath, "nogit", ".env.prod"),
			cfgName: "generic",
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					StartLine:   4,
					EndLine:     4,
					StartColumn: 5,
					EndColumn:   35,
					Line:        "\nDB_PASSWORD=8ae31cacf141669ddfb5da",
					Match:       "PASSWORD=8ae31cacf141669ddfb5da",
					Secret:      "8ae31cacf141669ddfb5da",
					File:        "../testdata/repos/nogit/.env.prod",
					Tags:        []string{},
					Entropy:     3.5383105,
					Fingerprint: "../testdata/repos/nogit/.env.prod:generic-api-key:4",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			cfg, _ := vc.Translate()
			detector := NewDetector(cfg)

			info, err := os.Stat(tt.source)
			require.NoError(t, err)

			var ignorePath string
			if info.IsDir() {
				ignorePath = filepath.Join(tt.source, ".gitleaksignore")
			} else {
				ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
			}
			err = detector.AddGitleaksIgnore(ignorePath)
			require.NoError(t, err)

			detector.FollowSymlinks = true
			paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
			require.NoError(t, err)

			findings, err := detector.DetectFiles(paths)
			require.NoError(t, err)

			// TODO: Temporary mitigation.
			// https://github.com/gitleaks/gitleaks/issues/1641
			normalizedFindings := make([]report.Finding, len(findings))
			for i, f := range findings {
				if strings.HasSuffix(f.Line, "\r") {
					f.Line = strings.ReplaceAll(f.Line, "\r", "")
				}
				if strings.HasSuffix(f.Match, "\r") {
					f.EndColumn = f.EndColumn - 1
					f.Match = strings.ReplaceAll(f.Match, "\r", "")
				}
				normalizedFindings[i] = f
			}
			assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
		})
	}
}

func TestDetectWithArchives(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		expireContext    bool
		expectedFindings []report.Finding
	}{
		{
			source:           filepath.Join(archivesBasePath, "this-path-does-not-exist"),
			cfgName:          "archives",
			expectedFindings: []report.Finding{},
		},
		{
			source:  filepath.Join(archivesBasePath, "files"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.gz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.gz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.xz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.xz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.zst",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.zst:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.7z"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.7z!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.7z!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.7z!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.7z!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.tar"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.tar.xz"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.xz!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar.xz!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.xz!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar.xz!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.tar.zst"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.zst!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar.zst!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.tar.zst!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.tar.zst!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.zip"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.zip!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.zip!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.zip!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.zip!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "nested.tar.gz"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.tar!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.tar!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.zip!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.zip!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.7z!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.7z!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files/main.go.xz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.xz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files/main.go.zst",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.zst:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files/main.go.gz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.gz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:           filepath.Join(archivesBasePath, "nested.tar.gz"),
			cfgName:          "archives",
			expireContext:    true,
			expectedFindings: []report.Finding{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			ctx, cancel := context.WithCancel(context.Background())
			if tt.expireContext {
				cancel()
			}

			cfg, _ := vc.Translate()
			detector := NewDetectorContext(ctx, cfg)
			detector.MaxArchiveDepth = 8

			findings, err := detector.DetectSource(
				ctx, &sources.Files{
					Path:            tt.source,
					Sema:            detector.Sema,
					Config:          &cfg,
					MaxArchiveDepth: detector.MaxArchiveDepth,
				},
			)

			if tt.expireContext {
				require.EqualError(t, err, "context canceled")
			} else {
				cancel()
				require.NoError(t, err)
			}

			// TODO: Temporary mitigation.
			// https://github.com/gitleaks/gitleaks/issues/1641
			normalizedFindings := make([]report.Finding, len(findings))
			for i, f := range findings {
				if strings.HasSuffix(f.Line, "\r") {
					f.Line = strings.ReplaceAll(f.Line, "\r", "")
				}
				if strings.HasSuffix(f.Match, "\r") {
					f.EndColumn = f.EndColumn - 1
					f.Match = strings.ReplaceAll(f.Match, "\r", "")
				}
				normalizedFindings[i] = f
			}
			assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
		})
	}

}

func TestDetectWithSymlinks(t *testing.T) {
	// TODO: Fix this test on windows.
	if runtime.GOOS == "windows" {
		t.Skipf("TODO: this returns no results on windows, I'm not sure why.")
		return
	}

	tests := []struct {
		cfgName          string
		source           string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "symlinks/file_symlink"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "apkey",
					Description: "Asymmetric Private Key",
					StartLine:   1,
					EndLine:     1,
					StartColumn: 1,
					EndColumn:   35,
					Match:       "-----BEGIN OPENSSH PRIVATE KEY-----",
					Secret:      "-----BEGIN OPENSSH PRIVATE KEY-----",
					Line:        "-----BEGIN OPENSSH PRIVATE KEY-----",
					File:        "../testdata/repos/symlinks/source_file/id_ed25519",
					SymlinkFile: "../testdata/repos/symlinks/file_symlink/symlinked_id_ed25519",
					Tags:        []string{"key", "AsymmetricPrivateKey"},
					Entropy:     3.587164,
					Fingerprint: "../testdata/repos/symlinks/source_file/id_ed25519:apkey:1",
				},
			},
		},
	}

	for _, tt := range tests {
		viper.AddConfigPath(configPath)
		viper.SetConfigName("simple")
		viper.SetConfigType("toml")
		err := viper.ReadInConfig()
		require.NoError(t, err)

		var vc config.ViperConfig
		err = viper.Unmarshal(&vc)
		require.NoError(t, err)

		cfg, _ := vc.Translate()
		detector := NewDetector(cfg)
		detector.FollowSymlinks = true
		paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
		require.NoError(t, err)

		findings, err := detector.DetectFiles(paths)
		require.NoError(t, err)
		assert.ElementsMatch(t, tt.expectedFindings, findings)
	}
}

func TestDetectRuleAllowlist(t *testing.T) {
	cases := map[string]struct {
		fragment  Fragment
		allowlist *config.Allowlist
		expected  []report.Finding
	}{
		// Commit / path
		"commit allowed": {
			fragment: Fragment{
				CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
			},
			allowlist: &config.Allowlist{
				Commits: []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
			},
		},
		"path allowed": {
			fragment: Fragment{
				FilePath: "package-lock.json",
			},
			allowlist: &config.Allowlist{
				Paths: []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
			},
		},
		"commit AND path allowed": {
			fragment: Fragment{
				CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
				FilePath:  "package-lock.json",
			},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Commits:        []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
				Paths:          []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
			},
		},
		"commit AND path NOT allowed": {
			fragment: Fragment{
				CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
				FilePath:  "package.json",
			},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Commits:        []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
				Paths:          []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
			},
			expected: []report.Finding{
				{
					StartLine:   1,
					EndLine:     1,
					StartColumn: 18,
					EndColumn:   28,
					Line:        "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
					Match:       "Summer2024!",
					Secret:      "Summer2024!",
					File:        "package.json",
					Commit:      "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
					Entropy:     3.095795154571533,
					RuleID:      "test-rule",
				},
			},
		},
		"commit AND path NOT allowed - other conditions": {
			fragment: Fragment{
				CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
				FilePath:  "package-lock.json",
			},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Commits:        []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
				Paths:          []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
				Regexes:        []*regexp.Regexp{regexp.MustCompile("password")},
			},
			expected: []report.Finding{
				{
					StartLine:   1,
					EndLine:     1,
					StartColumn: 18,
					EndColumn:   28,
					Line:        "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
					Match:       "Summer2024!",
					Secret:      "Summer2024!",
					File:        "package-lock.json",
					Commit:      "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
					Entropy:     3.095795154571533,
					RuleID:      "test-rule",
				},
			},
		},
		"commit OR path allowed": {
			fragment: Fragment{
				CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
				FilePath:  "package-lock.json",
			},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchOr,
				Commits:        []string{"704178e7dca77ff143778a31cff0fc192d59b030"},
				Paths:          []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
			},
		},

		// Regex / stopwords
		"regex allowed": {
			fragment: Fragment{},
			allowlist: &config.Allowlist{
				Regexes: []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
			},
		},
		"stopwords allowed": {
			fragment: Fragment{},
			allowlist: &config.Allowlist{
				StopWords: []string{"summer"},
			},
		},
		"regex AND stopword allowed": {
			fragment: Fragment{},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Regexes:        []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
				StopWords:      []string{"2024"},
			},
		},
		"regex AND stopword allowed - other conditions": {
			fragment: Fragment{
				CommitSHA: "41edf1f7f612199f401ccfc3144c2ebd0d7aeb48",
				FilePath:  "config.js",
			},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Commits:        []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
				Paths:          []*regexp.Regexp{regexp.MustCompile(`config.js`)},
				Regexes:        []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
				StopWords:      []string{"2024"},
			},
		},
		"regex AND stopword NOT allowed - non-git, other conditions": {
			fragment: Fragment{
				FilePath: "config.js",
			},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Commits:        []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
				Paths:          []*regexp.Regexp{regexp.MustCompile(`config.js`)},
				Regexes:        []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
				StopWords:      []string{"2024"},
			},
			expected: []report.Finding{
				{
					StartLine:   1,
					EndLine:     1,
					StartColumn: 18,
					EndColumn:   28,
					Line:        "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
					Match:       "Summer2024!",
					Secret:      "Summer2024!",
					File:        "config.js",
					Entropy:     3.095795154571533,
					RuleID:      "test-rule",
				},
			},
		},
		"regex AND stopword NOT allowed": {
			fragment: Fragment{},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Regexes: []*regexp.Regexp{
					regexp.MustCompile(`(?i)winter.+`),
				},
				StopWords: []string{"2024"},
			},
			expected: []report.Finding{
				{
					StartLine:   1,
					EndLine:     1,
					StartColumn: 18,
					EndColumn:   28,
					Line:        "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
					Match:       "Summer2024!",
					Secret:      "Summer2024!",
					Entropy:     3.095795154571533,
					RuleID:      "test-rule",
				},
			},
		},
		"regex AND stopword NOT allowed - other conditions": {
			fragment: Fragment{
				CommitSHA: "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b",
				FilePath:  "config.js",
			},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchAnd,
				Commits:        []string{"41edf1f7f612199f401ccfc3144c2ebd0d7aeb48"},
				Paths:          []*regexp.Regexp{regexp.MustCompile(`package-lock.json`)},
				Regexes:        []*regexp.Regexp{regexp.MustCompile(`(?i)winter.+`)},
				StopWords:      []string{"2024"},
			},
			expected: []report.Finding{
				{
					StartLine:   1,
					EndLine:     1,
					StartColumn: 18,
					EndColumn:   28,
					Line:        "let username = 'james@mail.com';\nlet password = 'Summer2024!';",
					Match:       "Summer2024!",
					Secret:      "Summer2024!",
					File:        "config.js",
					Commit:      "a060c9d2d5e90c992763f1bd4c3cd2a6f121241b",
					Entropy:     3.095795154571533,
					RuleID:      "test-rule",
				},
			},
		},
		"regex OR stopword allowed": {
			fragment: Fragment{},
			allowlist: &config.Allowlist{
				MatchCondition: config.AllowlistMatchOr,
				Regexes:        []*regexp.Regexp{regexp.MustCompile(`(?i)summer.+`)},
				StopWords:      []string{"winter"},
			},
		},
	}

	raw := `let username = 'james@mail.com';
let password = 'Summer2024!';`
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			err := tc.allowlist.Validate()
			require.NoError(t, err)

			rule := config.Rule{
				RuleID: "test-rule",
				Regex:  regexp.MustCompile(`Summer2024!`),
				Allowlists: []*config.Allowlist{
					tc.allowlist,
				},
			}
			d, err := NewDetectorDefaultConfig()
			require.NoError(t, err)

			f := tc.fragment
			f.Raw = raw

			actual := d.detectRule(f, raw, rule, []*codec.EncodedSegment{})
			compare(t, tc.expected, actual)
		})
	}
}

func moveDotGit(t *testing.T, from, to string) {
	t.Helper()

	repoDirs, err := os.ReadDir("../testdata/repos")
	require.NoError(t, err)
	for _, dir := range repoDirs {
		if to == ".git" {
			_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), "dotGit"))
			if os.IsNotExist(err) {
				// dont want to delete the only copy of .git accidentally
				continue
			}
			_ = os.RemoveAll(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), ".git"))
		}
		if !dir.IsDir() {
			continue
		}
		_, err := os.Stat(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from))
		if os.IsNotExist(err) {
			continue
		}

		err = os.Rename(fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), from),
			fmt.Sprintf("%s/%s/%s", repoBasePath, dir.Name(), to))
		require.NoError(t, err)
	}
}

// region Windows-specific tests[]
func TestNormalizeGitleaksIgnorePaths(t *testing.T) {
	d, err := NewDetectorDefaultConfig()
	require.NoError(t, err)

	err = d.AddGitleaksIgnore("../testdata/gitleaksignore/.windowspaths")
	require.NoError(t, err)

	assert.Len(t, d.gitleaksIgnore, 3)
	expected := map[string]struct{}{
		"foo/bar/gitleaks-false-positive.yaml:aws-access-token:4":                                                 {},
		"foo/bar/gitleaks-false-positive.yaml:aws-access-token:5":                                                 {},
		"b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data/test_local_repo_three_leaks.json:aws-access-token:73": {},
	}
	assert.ElementsMatch(t, maps.Keys(d.gitleaksIgnore), maps.Keys(expected))
}

func TestWindowsFileSeparator_RulePath(t *testing.T) {
	unixRule := config.Rule{
		RuleID: "test-rule",
		Path:   regexp.MustCompile(`(^|/)\.m2/settings\.xml`),
	}
	windowsRule := config.Rule{
		RuleID: "test-rule",
		Path:   regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`),
	}
	expected := []report.Finding{
		{
			RuleID: "test-rule",
			Match:  "file detected: .m2/settings.xml",
			File:   ".m2/settings.xml",
		},
	}
	tests := map[string]struct {
		fragment Fragment
		rule     config.Rule
		expected []report.Finding
	}{
		// unix rule
		"unix rule - unix path separator": {
			fragment: Fragment{
				FilePath: `.m2/settings.xml`,
			},
			rule:     unixRule,
			expected: expected,
		},
		"unix rule - windows path separator": {
			fragment: Fragment{
				FilePath:        `.m2/settings.xml`,
				WindowsFilePath: `.m2\settings.xml`,
			},
			rule:     unixRule,
			expected: expected,
		},
		"unix regex+path rule - windows path separator": {
			fragment: Fragment{
				Raw:      `<password>s3cr3t</password>`,
				FilePath: `.m2/settings.xml`,
			},
			rule: config.Rule{
				RuleID: "test-rule",
				Regex:  regexp.MustCompile(`<password>(.+?)</password>`),
				Path:   regexp.MustCompile(`(^|/)\.m2/settings\.xml`),
			},
			expected: []report.Finding{
				{
					RuleID:      "test-rule",
					StartColumn: 1,
					EndColumn:   27,
					Line:        "<password>s3cr3t</password>",
					Match:       "<password>s3cr3t</password>",
					Secret:      "s3cr3t",
					Entropy:     2.251629114151001,
					File:        ".m2/settings.xml",
				},
			},
		},

		// windows rule
		"windows rule - unix path separator": {
			fragment: Fragment{
				FilePath: `.m2/settings.xml`,
			},
			rule: windowsRule,
			// This never worked, and continues not to work.
			// Paths should be normalized to use Unix file separators.
			expected: nil,
		},
		"windows rule - windows path separator": {
			fragment: Fragment{
				FilePath:        `.m2/settings.xml`,
				WindowsFilePath: `.m2\settings.xml`,
			},
			rule:     windowsRule,
			expected: expected,
		},
		"windows regex+path rule - windows path separator": {
			fragment: Fragment{
				Raw:             `<password>s3cr3t</password>`,
				FilePath:        `.m2/settings.xml`,
				WindowsFilePath: `.m2\settings.xml`,
			},
			rule: config.Rule{
				RuleID: "test-rule",
				Regex:  regexp.MustCompile(`<password>(.+?)</password>`),
				Path:   regexp.MustCompile(`(^|\\)\.m2\\settings\.xml`),
			},
			expected: []report.Finding{
				{
					RuleID:      "test-rule",
					StartColumn: 1,
					EndColumn:   27,
					Line:        "<password>s3cr3t</password>",
					Match:       "<password>s3cr3t</password>",
					Secret:      "s3cr3t",
					Entropy:     2.251629114151001,
					File:        ".m2/settings.xml",
				},
			}},
	}

	d, err := NewDetectorDefaultConfig()
	require.NoError(t, err)
	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{})
			compare(t, test.expected, actual)
		})
	}
}

func TestWindowsFileSeparator_RuleAllowlistPaths(t *testing.T) {
	tests := map[string]struct {
		fragment Fragment
		rule     config.Rule
		expected []report.Finding
	}{
		// unix
		"unix path separator - unix rule - OR allowlist path-only": {
			fragment: Fragment{
				Raw:      `value: "s3cr3t"`,
				FilePath: `ignoreme/unix.txt`,
			},
			rule: config.Rule{
				RuleID: "unix-rule",
				Regex:  regexp.MustCompile(`s3cr3t`),
				Allowlists: []*config.Allowlist{
					{
						Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
					},
				},
			},
			expected: nil,
		},
		"unix path separator - windows rule - OR allowlist path-only": {
			fragment: Fragment{
				Raw:      `value: "s3cr3t"`,
				FilePath: `ignoreme/unix.txt`,
			},
			rule: config.Rule{
				RuleID: "windows-rule",
				Regex:  regexp.MustCompile(`s3cr3t`),
				Allowlists: []*config.Allowlist{
					{
						Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
					},
				},
			},
			// Windows separators in regex don't work for unix.
			expected: []report.Finding{
				{
					RuleID:      "windows-rule",
					StartColumn: 9,
					EndColumn:   14,
					Line:        `value: "s3cr3t"`,
					Match:       `s3cr3t`,
					Secret:      `s3cr3t`,
					File:        "ignoreme/unix.txt",
					Entropy:     2.251629114151001,
				},
			},
		},
		"unix path separator - unix rule - AND allowlist path+stopwords": {
			fragment: Fragment{
				Raw:      `value: "f4k3s3cr3t"`,
				FilePath: `ignoreme/unix.txt`,
			},
			rule: config.Rule{
				RuleID: "unix-rule",
				Regex:  regexp.MustCompile(`value: "[^"]+"`),
				Allowlists: []*config.Allowlist{
					{
						MatchCondition: config.AllowlistMatchAnd,
						Paths:          []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
						StopWords:      []string{"f4k3"},
					},
				},
			},
			expected: nil,
		},
		"unix path separator - windows rule - AND allowlist path+stopwords": {
			fragment: Fragment{
				Raw:      `value: "f4k3s3cr3t"`,
				FilePath: `ignoreme/unix.txt`,
			},
			rule: config.Rule{
				RuleID: "windows-rule",
				Regex:  regexp.MustCompile(`value: "[^"]+"`),
				Allowlists: []*config.Allowlist{
					{
						MatchCondition: config.AllowlistMatchAnd,
						Paths:          []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
						StopWords:      []string{"f4k3"},
					},
				},
			},
			expected: []report.Finding{
				{
					RuleID:      "windows-rule",
					StartColumn: 1,
					EndColumn:   19,
					Line:        `value: "f4k3s3cr3t"`,
					Match:       `value: "f4k3s3cr3t"`,
					Secret:      `value: "f4k3s3cr3t"`,
					File:        "ignoreme/unix.txt",
					Entropy:     3.892407178878784,
				},
			},
		},

		// windows
		"windows path separator - unix rule - OR allowlist path-only": {
			fragment: Fragment{
				Raw:             `value: "s3cr3t"`,
				FilePath:        `ignoreme/windows.txt`,
				WindowsFilePath: `ignoreme\windows.txt`,
			},
			rule: config.Rule{
				RuleID: "unix-rule",
				Regex:  regexp.MustCompile(`s3cr3t`),
				Allowlists: []*config.Allowlist{
					{
						Paths: []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
					},
				},
			},
			expected: nil,
		},
		"windows path separator - windows rule - OR allowlist path-only": {
			fragment: Fragment{
				Raw:             `value: "s3cr3t"`,
				FilePath:        `ignoreme/windows.txt`,
				WindowsFilePath: `ignoreme\windows.txt`,
			},
			rule: config.Rule{
				RuleID: "windows-rule",
				Regex:  regexp.MustCompile(`s3cr3t`),
				Allowlists: []*config.Allowlist{
					{
						Paths: []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
					},
				},
			},
			expected: nil,
		},
		"windows path separator - unix rule - AND allowlist path+stopwords": {
			fragment: Fragment{
				Raw:             `value: "f4k3s3cr3t"`,
				FilePath:        `ignoreme/unix.txt`,
				WindowsFilePath: `ignoreme\windows.txt`,
			},
			rule: config.Rule{
				RuleID: "unix-rule",
				Regex:  regexp.MustCompile(`value: "[^"]+"`),
				Allowlists: []*config.Allowlist{
					{
						MatchCondition: config.AllowlistMatchAnd,
						Paths:          []*regexp.Regexp{regexp.MustCompile(`(^|/)ignoreme(/.*)?$`)},
						StopWords:      []string{"f4k3"},
					},
				},
			},
			expected: nil,
		},
		"windows path separator - windows rule - AND allowlist path+stopwords": {
			fragment: Fragment{
				Raw:             `value: "f4k3s3cr3t"`,
				FilePath:        `ignoreme/unix.txt`,
				WindowsFilePath: `ignoreme\windows.txt`,
			},
			rule: config.Rule{
				RuleID: "windows-rule",
				Regex:  regexp.MustCompile(`value: "[^"]+"`),
				Allowlists: []*config.Allowlist{
					{
						MatchCondition: config.AllowlistMatchAnd,
						Paths:          []*regexp.Regexp{regexp.MustCompile(`(^|\\)ignoreme(\\.*)?$`)},
						StopWords:      []string{"f4k3"},
					},
				},
			},
			expected: nil,
		},
	}

	d, err := NewDetectorDefaultConfig()
	require.NoError(t, err)
	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			actual := d.detectRule(test.fragment, test.fragment.Raw, test.rule, []*codec.EncodedSegment{})
			compare(t, test.expected, actual)
		})
	}
}

[evidence record sha256:d6979345d106bc2460da2977fff12289c6895d166196dd37fa132b41e045a6a1 kind tool-call:read]
step 11: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/location.go"}
tool read ok: package detect

// Location represents a location in a file
type Location struct {
	startLine      int
	endLine        int
	startColumn    int
	endColumn      int
	startLineIndex int
	endLineIndex   int
}

func location(newlineIndices [][]int, raw string, matchIndex []int) Location {
	var (
		prevNewLine int
		location    Location
		lineSet     bool
		_lineNum    int
	)

	start := matchIndex[0]
	end := matchIndex[1]

	// default startLineIndex to 0
	location.startLineIndex = 0

	// Fixes: https://github.com/zricethezav/gitleaks/issues/1037
	// When a fragment does NOT have any newlines, a default "newline"
	// will be counted to make the subsequent location calculation logic work
	// for fragments will no newlines.
	if len(newlineIndices) == 0 {
		newlineIndices = [][]int{
			{len(raw), len(raw) + 1},
		}
	}

	for lineNum, pair := range newlineIndices {
		_lineNum = lineNum
		newLineByteIndex := pair[0]
		if prevNewLine <= start && start < newLineByteIndex {
			lineSet = true
			location.startLine = lineNum
			location.endLine = lineNum
			location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1
			location.startLineIndex = prevNewLine
			location.endLineIndex = newLineByteIndex
		}
		if prevNewLine < end && end <= newLineByteIndex {
			location.endLine = lineNum
			location.endColumn = (end - prevNewLine)
			location.endLineIndex = newLineByteIndex
		}

		prevNewLine = pair[0]
	}

	if !lineSet {
		// if lines never get set then that means the secret is most likely
		// on the last line of the diff output and the diff output does not have
		// a newline
		location.startColumn = (start - prevNewLine) + 1 // +1 because counting starts at 1
		location.endColumn = (end - prevNewLine)
		location.startLine = _lineNum + 1
		location.endLine = _lineNum + 1

		// search for new line byte index
		i := 0
		for end+i < len(raw) {
			if raw[end+i] == '\n' {
				break
			}
			if raw[end+i] == '\r' {
				break
			}
			i++
		}
		location.endLineIndex = end + i
	}
	return location
}

[evidence record sha256:42769314811f0c421c5ce8391e2a70a391ca5fe593ee5d916cd593f95cbf473d kind tool-call:read]
tool read <- {"path":"detect/utils.go"}
tool read ok: package detect

import (
	// "encoding/json"
	"fmt"
	"math"
	"path/filepath"
	"strings"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"

	"github.com/charmbracelet/lipgloss"
)

var linkCleaner = strings.NewReplacer(
	" ", "%20",
	"%", "%25",
)

func createScmLink(remote *sources.RemoteInfo, finding report.Finding) string {
	if remote.Platform == scm.UnknownPlatform ||
		remote.Platform == scm.NoPlatform ||
		finding.Commit == "" {
		return ""
	}

	// Clean the path.
	filePath, _, hasInnerPath := strings.Cut(finding.File, sources.InnerPathSeparator)
	filePath = linkCleaner.Replace(filePath)

	switch remote.Platform {
	case scm.GitHubPlatform:
		link := fmt.Sprintf("%s/blob/%s/%s", remote.Url, finding.Commit, filePath)
		if hasInnerPath {
			return link
		}
		ext := strings.ToLower(filepath.Ext(filePath))
		if ext == ".ipynb" || ext == ".md" {
			link += "?plain=1"
		}
		if finding.StartLine != 0 {
			link += fmt.Sprintf("#L%d", finding.StartLine)
		}
		if finding.EndLine != finding.StartLine {
			link += fmt.Sprintf("-L%d", finding.EndLine)
		}
		return link
	case scm.GitLabPlatform:
		link := fmt.Sprintf("%s/blob/%s/%s", remote.Url, finding.Commit, filePath)
		if hasInnerPath {
			return link
		}
		if finding.StartLine != 0 {
			link += fmt.Sprintf("#L%d", finding.StartLine)
		}
		if finding.EndLine != finding.StartLine {
			link += fmt.Sprintf("-%d", finding.EndLine)
		}
		return link
	case scm.AzureDevOpsPlatform:
		link := fmt.Sprintf("%s/commit/%s?path=/%s", remote.Url, finding.Commit, filePath)
		// Add line information if applicable
		if hasInnerPath {
			return link
		}
		if finding.StartLine != 0 {
			link += fmt.Sprintf("&line=%d", finding.StartLine)
		}
		if finding.EndLine != finding.StartLine {
			link += fmt.Sprintf("&lineEnd=%d", finding.EndLine)
		}
		// This is a bit dirty, but Azure DevOps does not highlight the line when the lineStartColumn and lineEndColumn are not provided
		link += "&lineStartColumn=1&lineEndColumn=10000000&type=2&lineStyle=plain&_a=files"
		return link
	case scm.GiteaPlatform:
		link := fmt.Sprintf("%s/src/commit/%s/%s", remote.Url, finding.Commit, filePath)
		if hasInnerPath {
			return link
		}
		ext := strings.ToLower(filepath.Ext(filePath))
		if ext == ".ipynb" || ext == ".md" {
			link += "?display=source"
		}
		if finding.StartLine != 0 {
			link += fmt.Sprintf("#L%d", finding.StartLine)
		}
		if finding.EndLine != finding.StartLine {
			link += fmt.Sprintf("-L%d", finding.EndLine)
		}
		return link
	case scm.BitbucketPlatform:
		link := fmt.Sprintf("%s/src/%s/%s", remote.Url, finding.Commit, filePath)
		if hasInnerPath {
			return link
		}
		if finding.StartLine != 0 {
			link += fmt.Sprintf("#lines-%d", finding.StartLine)
		}
		if finding.EndLine != finding.StartLine {
			link += fmt.Sprintf(":%d", finding.EndLine)
		}
		return link
	default:
		// This should never happen.
		return ""
	}
}

// shannonEntropy calculates the entropy of data using the formula defined here:
// https://en.wiktionary.org/wiki/Shannon_entropy
// Another way to think about what this is doing is calculating the number of bits
// needed to on average encode the data. So, the higher the entropy, the more random the data, the
// more bits needed to encode that data.
func shannonEntropy(data string) (entropy float64) {
	if data == "" {
		return 0
	}

	charCounts := make(map[rune]int)
	for _, char := range data {
		charCounts[char]++
	}

	invLength := 1.0 / float64(len(data))
	for _, count := range charCounts {
		freq := float64(count) * invLength
		entropy -= freq * math.Log2(freq)
	}

	return entropy
}

// filter will dedupe and redact findings
func filter(findings []report.Finding, redact uint) []report.Finding {
	var retFindings []report.Finding
	for _, f := range findings {
		include := true
		if strings.Contains(strings.ToLower(f.RuleID), "generic") {
			for _, fPrime := range findings {
				if f.StartLine == fPrime.StartLine &&
					f.Commit == fPrime.Commit &&
					f.RuleID != fPrime.RuleID &&
					strings.Contains(fPrime.Secret, f.Secret) &&
					!strings.Contains(strings.ToLower(fPrime.RuleID), "generic") {

					genericMatch := strings.ReplaceAll(f.Match, f.Secret, "REDACTED")
					betterMatch := strings.ReplaceAll(fPrime.Match, fPrime.Secret, "REDACTED")
					logging.Trace().Msgf("skipping %s finding (%s), %s rule takes precedence (%s)", f.RuleID, genericMatch, fPrime.RuleID, betterMatch)
					include = false
					break
				}
			}
		}

		if redact > 0 {
			f.Redact(redact)
		}
		if include {
			retFindings = append(retFindings, f)
		}
	}
	return retFindings
}

func printFinding(f report.Finding, noColor bool) {
	// trim all whitespace and tabs
	f.Line = strings.TrimSpace(f.Line)
	f.Secret = strings.TrimSpace(f.Secret)
	f.Match = strings.TrimSpace(f.Match)

	isFileMatch := strings.HasPrefix(f.Match, "file detected:")
	skipColor := noColor
	finding := ""
	var secret lipgloss.Style

	// Matches from filenames do not have a |line| or |secret|
	if !isFileMatch {
		matchInLineIDX := strings.Index(f.Line, f.Match)
		secretInMatchIdx := strings.Index(f.Match, f.Secret)

		skipColor = false

		if matchInLineIDX == -1 || noColor {
			skipColor = true
			matchInLineIDX = 0
		}

		start := f.Line[0:matchInLineIDX]
		startMatchIdx := 0
		if matchInLineIDX > 20 {
			startMatchIdx = matchInLineIDX - 20
			start = "..." + f.Line[startMatchIdx:matchInLineIDX]
		}

		matchBeginning := lipgloss.NewStyle().SetString(f.Match[0:secretInMatchIdx]).Foreground(lipgloss.Color("#f5d445"))
		secret = lipgloss.NewStyle().SetString(f.Secret).
			Bold(true).
			Italic(true).
			Foreground(lipgloss.Color("#f05c07"))
		matchEnd := lipgloss.NewStyle().SetString(f.Match[secretInMatchIdx+len(f.Secret):]).Foreground(lipgloss.Color("#f5d445"))

		lineEndIdx := matchInLineIDX + len(f.Match)
		if len(f.Line)-1 <= lineEndIdx {
			lineEndIdx = len(f.Line)
		}

		lineEnd := f.Line[lineEndIdx:]

		if len(f.Secret) > 100 {
			secret = lipgloss.NewStyle().SetString(f.Secret[0:100] + "...").
				Bold(true).
				Italic(true).
				Foreground(lipgloss.Color("#f05c07"))
		}
		if len(lineEnd) > 20 {
			lineEnd = lineEnd[0:20] + "..."
		}

		finding = fmt.Sprintf("%s%s%s%s%s\n", strings.TrimPrefix(strings.TrimLeft(start, " "), "\n"), matchBeginning, secret, matchEnd, lineEnd)
	}

	if skipColor || isFileMatch {
		fmt.Printf("%-12s %s\n", "Finding:", f.Match)
		fmt.Printf("%-12s %s\n", "Secret:", f.Secret)
	} else {
		fmt.Printf("%-12s %s", "Finding:", finding)
		fmt.Printf("%-12s %s\n", "Secret:", secret)
	}

	fmt.Printf("%-12s %s\n", "RuleID:", f.RuleID)
	fmt.Printf("%-12s %f\n", "Entropy:", f.Entropy)

	if f.File == "" {
		f.PrintRequiredFindings()
		fmt.Println("")
		return
	}
	if len(f.Tags) > 0 {
		fmt.Printf("%-12s %s\n", "Tags:", f.Tags)
	}
	fmt.Printf("%-12s %s\n", "File:", f.File)
	fmt.Printf("%-12s %d\n", "Line:", f.StartLine)
	if f.Commit == "" {
		fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
		f.PrintRequiredFindings()
		fmt.Println("")
		return
	}
	fmt.Printf("%-12s %s\n", "Commit:", f.Commit)
	fmt.Printf("%-12s %s\n", "Author:", f.Author)
	fmt.Printf("%-12s %s\n", "Email:", f.Email)
	fmt.Printf("%-12s %s\n", "Date:", f.Date)
	fmt.Printf("%-12s %s\n", "Fingerprint:", f.Fingerprint)
	if f.Link != "" {
		fmt.Printf("%-12s %s\n", "Link:", f.Link)
	}

	f.PrintRequiredFindings()
	fmt.Println("")
}

[evidence record sha256:0f503ab428f1c39a5e1c2876a616e207d3ffef03db66de6f43fd869458b12b81 kind tool-call:read]
step 12: calling local:qwen3.6:35b-a3b
tool list <- {"path":"testdata/config"}
tool list ok: archives.toml
composite.toml
encoded.toml
generic.toml
generic_with_py_path.toml
invalid/
simple.toml
valid/
[evidence record sha256:7decfbcedb4eb9af5e4d4518167a98d98b37420eb15da3f573d62ecae5d214a4 kind tool-call:list]
step 13: calling local:qwen3.6:35b-a3b
tool read <- {"path":"testdata/config/simple.toml"}
tool read ok: title = "gitleaks config"
# https://learnxinyminutes.com/docs/toml/ for toml reference

[[rules]]
    description = "1Password Secret Key"
    id = "1password-secret-key"
    regex = '''A3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}'''
    tags = ["1Password"]

[[rules]]
    description = "AWS Access Key"
    id = "aws-access-key"
    regex = '''(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}'''
    tags = ["key", "AWS"]

[[rules]]
    description = "AWS Secret Key"
    id = "aws-secret-key"
    regex = '''(?i)aws_(.{0,20})?=?.[\'\"0-9a-zA-Z\/+]{40}'''
    tags = ["key", "AWS"]

[[rules]]
    description = "AWS MWS key"
    id = "aws-mws-key"
    regex = '''amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
    tags = ["key", "AWS", "MWS"]

[[rules]]
    description = "Facebook Secret Key"
    id = "facebook-secret-key"
    regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]'''
    tags = ["key", "Facebook"]

[[rules]]
    description = "Facebook Client ID"
    id = "facebook-client-id"
    regex = '''(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]'''
    tags = ["key", "Facebook"]

[[rules]]
    description = "Twitter Secret Key"
    id = "twitter-secret-key"
    regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}['\"]'''
    tags = ["key", "Twitter"]

[[rules]]
    description = "Twitter Client ID"
    id = "twitter-client-id"
    regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}['\"]'''
    tags = ["client", "Twitter"]

[[rules]]
    description = "Github Personal Access Token"
    id = "github-pat"
    regex = '''ghp_[0-9a-zA-Z]{36}'''
    tags = ["key", "Github"]
[[rules]]
    description = "Github OAuth Access Token"
    id = "github-oauth"
    regex = '''gho_[0-9a-zA-Z]{36}'''
    tags = ["key", "Github"]
[[rules]]
    id = "github-app"
    description = "Github App Token"
    regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}'''
    tags = ["key", "Github"]
[[rules]]
    id = "github-refresh"
    description = "Github Refresh Token"
    regex = '''ghr_[0-9a-zA-Z]{76}'''
    tags = ["key", "Github"]

[[rules]]
    id = "linkedin-client"
    description = "LinkedIn Client ID"
    regex = '''(?i)linkedin(.{0,20})?(?-i)[0-9a-z]{12}'''
    tags = ["client", "LinkedIn"]

[[rules]]
    id = "linkedin-secret"
    description = "LinkedIn Secret Key"
    regex = '''(?i)linkedin(.{0,20})?[0-9a-z]{16}'''
    tags = ["secret", "LinkedIn"]

[[rules]]
    id = "slack"
    description = "Slack"
    regex = '''xox[baprs]-(?:[0-9a-zA-Z]{10,48})?'''
    tags = ["key", "Slack"]

[[rules]]
    id = "apkey"
    description = "Asymmetric Private Key"
    regex = '''-----BEGIN (?:(?:EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY(?: BLOCK)?-----'''
    tags = ["key", "AsymmetricPrivateKey"]

[[rules]]
    id = "google"
    description = "Google API key"
    regex = '''AIza[0-9A-Za-z\-_]{35}'''
    tags = ["key", "Google"]

[[rules]]
    id = "google"
    description = "Google (GCP) Service Account"
    regex = '''"type": "service_account"'''
    tags = ["key", "Google"]

[[rules]]
    id = "heroku"
    description = "Heroku API key"
    regex = '''(?i)heroku(.{0,20})?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'''
    tags = ["key", "Heroku"]

[[rules]]
    id = "mailchimp"
    description = "MailChimp API key"
    regex = '''(?i)(mailchimp|mc)(.{0,20})?[0-9a-f]{32}-us[0-9]{1,2}'''
    tags = ["key", "Mailchimp"]

[[rules]]
    id = "mailgun"
    description = "Mailgun API key"
    regex = '''((?i)(mailgun|mg)(.{0,20})?)?key-[0-9a-z]{32}'''
    tags = ["key", "Mailgun"]

[[rules]]
    id = "paypal"
    description = "PayPal Braintree access token"
    regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}'''
    tags = ["key", "Paypal"]

[[rules]]
    id = "piacatic"
    description = "Picatic API key"
    regex = '''sk_live_[0-9a-z]{32}'''
    tags = ["key", "Picatic"]

[[rules]]
    id = "sendgrid"
    description = "SendGrid API Key"
    regex = '''SG\.[\w_]{16,32}\.[\w_]{16,64}'''
    tags = ["key", "SendGrid"]

[[rules]]
    description = "Sidekiq Secret"
    id = "sidekiq-secret"
    regex = '''(?i)(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:['|\"|\n|\r|\s|\x60|;]|$)'''
    secretGroup = 1
    keywords = [
        "bundle_enterprise__contribsys__com","bundle_gems__contribsys__com",
    ]

[[rules]]
    description = "Sidekiq Sensitive URL"
    id = "sidekiq-sensitive-url"
    regex  = '''(?i)\b(http(?:s??):\/\/)([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)'''
    secretGroup = 2
    keywords = [
        "gems.contribsys.com","enterprise.contribsys.com",
    ]

[[rules]]
    id = "slack-webhook"
    description = "Slack Webhook"
    regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}'''
    tags = ["key", "slack"]

[[rules]]
    id = "stripe"
    description = "Stripe API key"
    regex = '''(?i)stripe(.{0,20})?[sr]k_live_[0-9a-zA-Z]{24}'''
    tags = ["key", "Stripe"]

[[rules]]
    id = "square"
    description = "Square access token"
    regex = '''sq0atp-[0-9A-Za-z\-_]{22}'''
    tags = ["key", "square"]

[[rules]]
    id = "square-oauth"
    description = "Square OAuth secret"
    regex = '''sq0csp-[0-9A-Za-z\-_]{43}'''
    tags = ["key", "square"]

[[rules]]
    id = "twilio"
    description = "Twilio API key"
    regex = '''(?i)twilio(.{0,20})?SK[0-9a-f]{32}'''
    tags = ["key", "twilio"]

[[rules]]
    id = "dynatrace"
    description = "Dynatrace ttoken"
    regex = '''dt0[a-zA-Z]{1}[0-9]{2}\.[A-Z0-9]{24}\.[A-Z0-9]{64}'''
    tags = ["key", "Dynatrace"]

[[rules]]
    id = "shopify"
    description = "Shopify shared secret"
    regex = '''shpss_[a-fA-F0-9]{32}'''
    tags = ["key", "Shopify"]

[[rules]]
    id = "shopify-access"
    description = "Shopify access token"
    regex = '''shpat_[a-fA-F0-9]{32}'''
    tags = ["key", "Shopify"]

[[rules]]
    id = "shopify-custom"
    description = "Shopify custom app access token"
    regex = '''shpca_[a-fA-F0-9]{32}'''
    tags = ["key", "Shopify"]

[[rules]]
    id = "shopify-private"
    description = "Shopify private app access token"
    regex = '''shppa_[a-fA-F0-9]{32}'''
    tags = ["key", "Shopify"]

[[rules]]
    id = "pypi"
    description = "PyPI upload token"
    regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}'''
    tags = ["key", "pypi"]

[evidence record sha256:b7987afcca17d510fce54de9fb0742563c10ea724cdc89f9625b763ab50eea50 kind tool-call:read]
tool read <- {"path":"testdata/config/composite.toml"}
tool read ok: title = "Fragment level composite rule"

[[rules]]
id = "primary-rule"
description = "Primary rule"
regex = 'password\s*=\s*"([^"]+)"'
[[rules.required]]
id = "username-rule"

[[rules]]
id = "username-rule"
description = "Username rule"
regex = 'username\s*=\s*"([^"]+)"'
skipReport = true
[evidence record sha256:fa7553a9f852826ed0c175e6cbfa00301ae3601edee0de3bc7b62ab86d9b3370 kind tool-call:read]
tool read <- {"path":"detect/codec"}
tool read failed: failed: EISDIR: illegal operation on a directory, read, caused by EISDIR
[evidence record sha256:91e796b462e18f666cfcc55e1c92d06c0a20f4d0becb7f610920de7d1dba9e8a kind tool-call:read]
step 14: calling local:qwen3.6:35b-a3b
tool list <- {"path":"report"}
tool list ok: constants.go
csv.go
csv_test.go
finding.go
finding_test.go
json.go
json_test.go
junit.go
junit_test.go
report.go
report_test.go
sarif.go
sarif_test.go
template.go
template_test.go
[evidence record sha256:92b6cbad5e2a16c3763cb10189cf2e49c348f5972ce53c9446c0dfcfc308415f kind tool-call:list]
tool read <- {"path":"detect/codec/decoder.go"}
tool read ok: package codec

import (
	"bytes"

	"github.com/zricethezav/gitleaks/v8/logging"
)

// Decoder decodes various types of data in place
type Decoder struct {
	decodedMap map[string]string
}

// NewDecoder creates a default decoder struct
func NewDecoder() *Decoder {
	return &Decoder{
		decodedMap: make(map[string]string),
	}
}

// Decode returns the data with the values decoded in place along with the
// encoded segment meta data for the next pass of decoding
func (d *Decoder) Decode(data string, predecessors []*EncodedSegment) (string, []*EncodedSegment) {
	segments := d.findEncodedSegments(data, predecessors)

	if len(segments) > 0 {
		result := bytes.NewBuffer(make([]byte, 0, len(data)))
		encodedStart := 0
		for _, segment := range segments {
			result.WriteString(data[encodedStart:segment.encoded.start])
			result.WriteString(segment.decodedValue)
			encodedStart = segment.encoded.end
		}

		result.WriteString(data[encodedStart:])
		return result.String(), segments
	}

	return data, segments
}

// findEncodedSegments finds the encoded segments in the data
func (d *Decoder) findEncodedSegments(data string, predecessors []*EncodedSegment) []*EncodedSegment {
	if len(data) == 0 {
		return []*EncodedSegment{}
	}

	decodedShift := 0
	encodingMatches := findEncodingMatches(data)
	segments := make([]*EncodedSegment, 0, len(encodingMatches))
	for _, m := range encodingMatches {
		encodedValue := data[m.start:m.end]
		decodedValue, alreadyDecoded := d.decodedMap[encodedValue]

		if !alreadyDecoded {
			decodedValue = m.encoding.decode(encodedValue)
			d.decodedMap[encodedValue] = decodedValue
		}

		if len(decodedValue) == 0 {
			continue
		}

		segment := &EncodedSegment{
			predecessors: predecessors,
			original:     toOriginal(predecessors, m.startEnd),
			encoded:      m.startEnd,
			decoded: startEnd{
				m.start + decodedShift,
				m.start + decodedShift + len(decodedValue),
			},
			decodedValue: decodedValue,
			encodings:    m.encoding.kind,
			depth:        1,
		}

		// Shift decoded start and ends based on size changes
		decodedShift += len(decodedValue) - len(encodedValue)

		// Adjust depth and encoding if applicable
		if len(segment.predecessors) != 0 {
			// Set the depth based on the predecessors' depth in the previous pass
			segment.depth = 1 + segment.predecessors[0].depth
			// Adjust encodings
			for _, p := range segment.predecessors {
				if segment.encoded.overlaps(p.decoded) {
					segment.encodings |= p.encodings
				}
			}
		}

		segments = append(segments, segment)
		logging.Debug().
			Str("decoder", m.encoding.kind.String()).
			Msgf(
				"segment found: original=%s pos=%s: %q -> %q",
				segment.original,
				segment.encoded,
				encodedValue,
				segment.decodedValue,
			)
	}

	return segments
}

[evidence record sha256:bcfab730e23a73e49f87dc59e88e68d41c7d2f077f3eb0ed24e0133c988796d4 kind tool-call:read]
step 15: calling local:qwen3.6:35b-a3b
tool list <- {"path":"config"}
tool list ok: allowlist.go
allowlist_test.go
config.go
config_test.go
gitleaks.toml
rule.go
utils.go
[evidence record sha256:a8b6b6a9dd2ef8d50369d24daa399e5b60c662b6cef31f5447f2482d34832fbf kind tool-call:list]
step 16: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"cat testdata/config/valid/rule_path_only.toml 2>&1"}
tool shell ok: exit code: 0
stdout:
[[rules]]
description = "Python Files"
id = "python-files-only"
path = '''.py'''
[evidence record sha256:80ca0e9cb1f644b47733716a18b2e7f5ac6d9b3b5437c6db34e920ca148107f0 kind tool-call:shell]
tool shell <- {"command":"cat testdata/gitleaksignore/.windowspaths 2>&1"}
tool shell ok: exit code: 0
stdout:
foo\bar\gitleaks-false-positive.yaml:aws-access-token:4
b55d88dc151f7022901cda41a03d43e0e508f2b7:test_data\test_local_repo_three_leaks.json:aws-access-token:73
foo/bar/gitleaks-false-positive.yaml:aws-access-token:5
# comment
[evidence record sha256:aebd85bb4b39e1964f3d316d59981190376589ebd9ca60e05203514d48478560 kind tool-call:shell]
step 17: calling local:qwen3.6:35b-a3b
tool read <- {"path":"report/finding.go"}
tool read ok: package report

import (
	"fmt"
	"math"
	"strings"

	"github.com/charmbracelet/lipgloss"
	"github.com/zricethezav/gitleaks/v8/sources"
)

// Finding contains a whole bunch of information about a secret finding.
// Plenty of real estate in this bad boy so fillerup as needed.
type Finding struct {
	// Rule is the name of the rule that was matched
	RuleID      string
	Description string

	StartLine   int
	EndLine     int
	StartColumn int
	EndColumn   int

	Line string `json:"-"`

	Match string

	// Captured secret
	Secret string

	// File is the name of the file containing the finding
	File        string
	SymlinkFile string
	Commit      string
	Link        string `json:",omitempty"`

	// Entropy is the shannon entropy of Value
	Entropy float32

	Author  string
	Email   string
	Date    string
	Message string
	Tags    []string

	// unique identifier
	Fingerprint string

	// Fragment used for multi-part rule checking, CEL filtering,
	// and eventually ML validation
	Fragment *sources.Fragment `json:",omitempty"`

	// TODO keeping private for now to during experimental phase
	requiredFindings []*RequiredFinding
}

type RequiredFinding struct {
	// contains a subset of the Finding fields
	// only used for reporting
	RuleID      string
	StartLine   int
	EndLine     int
	StartColumn int
	EndColumn   int
	Line        string `json:"-"`
	Match       string
	Secret      string
}

func (f *Finding) AddRequiredFindings(afs []*RequiredFinding) {
	if f.requiredFindings == nil {
		f.requiredFindings = make([]*RequiredFinding, 0)
	}
	f.requiredFindings = append(f.requiredFindings, afs...)
}

// Redact removes sensitive information from a finding.
func (f *Finding) Redact(percent uint) {
	secret := maskSecret(f.Secret, percent)
	if percent >= 100 {
		secret = "REDACTED"
	}
	f.Line = strings.ReplaceAll(f.Line, f.Secret, secret)
	f.Match = strings.ReplaceAll(f.Match, f.Secret, secret)
	f.Secret = secret
}

func maskSecret(secret string, percent uint) string {
	if percent > 100 {
		percent = 100
	}
	len := float64(len(secret))
	if len <= 0 {
		return secret
	}
	prc := float64(100 - percent)
	lth := int64(math.RoundToEven(len * prc / float64(100)))

	return secret[:lth] + "..."
}

func (f *Finding) PrintRequiredFindings() {
	if len(f.requiredFindings) == 0 {
		return
	}

	fmt.Printf("%-12s ", "Required:")

	// Create orange style for secrets
	orangeStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#bf9478"))

	for i, aux := range f.requiredFindings {
		auxSecret := strings.TrimSpace(aux.Secret)
		// Truncate long secrets for readability
		if len(auxSecret) > 40 {
			auxSecret = auxSecret[:37] + "..."
		}

		// Format: rule-id:line:secret
		if i == 0 {
			fmt.Printf("%s:%d:%s\n", aux.RuleID, aux.StartLine, orangeStyle.Render(auxSecret))
		} else {
			fmt.Printf("%-12s %s:%d:%s\n", "", aux.RuleID, aux.StartLine, orangeStyle.Render(auxSecret))
		}
	}
}

[evidence record sha256:efc91e927d8ac9ad04f030e707f4ef8a6918511203981f1cd9d1b2ce9b955212 kind tool-call:read]
step 18: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"pwd"}
tool shell ok: exit code: 0
stdout:
/work
[evidence record sha256:8c8957175961101b6224738076b1d848fcc85fca2a51641f548ecbb9db85ebed kind tool-call:shell]
step 19: calling local:qwen3.6:35b-a3b
tool search <- {"pattern":"func Test","path":"detect"}
tool search ok: detect/baseline_test.go:13: func TestIsNew(t *testing.T) {
detect/baseline_test.go:156: func TestFileLoadBaseline(t *testing.T) {
detect/baseline_test.go:182: func TestIgnoreIssuesInBaseline(t *testing.T) {
detect/codec/decoder_test.go:11: func TestDecode(t *testing.T) {
detect/detect_test.go:132: func TestDetect(t *testing.T) {
detect/detect_test.go:850: func TestFromGit(t *testing.T) {
detect/detect_test.go:1328: func TestFromGitStaged(t *testing.T) {
detect/detect_test.go:1400: func TestFromFiles(t *testing.T) {
detect/detect_test.go:1529: func TestDetectWithArchives(t *testing.T) {
detect/detect_test.go:2127: func TestDetectWithSymlinks(t *testing.T) {
detect/detect_test.go:2186: func TestDetectRuleAllowlist(t *testing.T) {
detect/detect_test.go:2461: func TestNormalizeGitleaksIgnorePaths(t *testing.T) {
detect/detect_test.go:2477: func TestWindowsFileSeparator_RulePath(t *testing.T) {
detect/detect_test.go:2591: func TestWindowsFileSeparator_RuleAllowlistPaths(t *testing.T) {
detect/location_test.go:10: func TestGetLocation(t *testing.T) {
detect/reader_test.go:42: func TestDetectReader(t *testing.T) {
detect/reader_test.go:78: func TestStreamDetectReader(t *testing.T) {
detect/utils_test.go:13: func Test_createScmLink(t *testing.T) {
[evidence record sha256:1f7726724f3b7b1973473b0a5a5bd0667933ebe045a6ec0d2c9d257b09f53e15 kind tool-call:search]
step 20: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"which go 2>&1 || echo \"not found\""}
tool shell failed: denied: "which go 2>&1 || echo "not found"" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:180391ca22aca7ca7312d1ed265cfc7f5280948e517063bd6b9a61676c9ca514 kind tool-call:shell]
step 21: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go"}
tool shell failed: denied: "go" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:bd317fea70fde3aae82073902897b0e84f02fa754d0e39f700ad3cbddf34c0d5 kind tool-call:shell]
stopped: max-tokens after 21 steps, 1011111 tokens
gate typecheck passed: the command exited 0 [evidence record sha256:e1c6608989b91f0967c6cc9f2fddf1c0fdc8a9fe258d27039681de83aaa7c274]
gate lint failed: the command exited 1 [evidence record sha256:0727ab3ea642edcbc4539f50eeac64455b8442a27034f43e48af1904b8562e0a]
gate format failed: the command listed 152 offending file(s) [evidence record sha256:dce8e676df5391d97b949c44ff881e767670b2d6b8b68fafb56a9fa686b32db7]
gate tests failed: the command exited 1 [evidence record sha256:c16c11f9dba24492f54d1868a7180cbe1d5ff618750fee5f06abe198baa028ed]
gate file-set passed: nothing changed and no file set was declared, so there is nothing to check [evidence record sha256:da941441683d2a932e3656d29e47bf1d3dd8e8827a4fa28340e9192147fc0140]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:04cbfbf05767e77d5767fb5772e9a7e62cec4ae74d421e013fe257c5eccb892c]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:1205c218e3298f2b2a033cfedf23b7ee3ea0c6b62794e4df970e745e5f7a0cb4]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:6c58dc822ecce8f4da2f5c78e5027adb86eef9574d10c896a93cc27d285aa5df]
gate diff-budget passed (advisory): within budget: 0 file(s) and 0 added line(s) [evidence record sha256:e3dbe12b4205d31760f1fad23748c5ea393fa58eb135b7d0aec249a7ae178e24]
auto-resolve attempt 1 of 2
step 1: calling local:qwen3.6:35b-a3b
plan: Looking at the failures, I need to address:

1. **Lint**: context leak in `detect/detect_test.go` (unused cancel function)
2. **Format**: many files need formatting (likely third-party code in `.campaign/gomod/`)
3. **Tests**: Regex compilation fails because `\c` is not a valid escape sequence

Let me start by examining the workspace structure and the relevant source files.
tool list <- {"path":"."}
tool list ok: .campaign/
.git/
.github/
.gitignore
.gitleaks.toml
.gitleaksignore
.golangci.yaml
.goreleaser.yml
.pre-commit-hooks.yaml
CONTRIBUTING.md
Dockerfile
LICENSE
Makefile
README.md
SECURITY.md
USERS.md
cmd/
config/
detect/
go.mod
go.sum
logging/
main.go
regexp/
report/
report_templates/
scripts/
sources/
testdata/
version/
[evidence record sha256:1eff9eb9b6be6f5488569f984f3107217c89fd8109f0a6e724f06abaaa8c8c99 kind tool-call:list]
tool read <- {"path":"detect/detect_test.go","maxBytes":20000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq env var, semicolon": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 21,
					EndColumn:   74,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq url": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-sensitive-url",
					Description: "Sidekiq Sensitive URL",
					File:        "tmp.sh",
					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
					Secret:      "cafeb4b3:d3adb33f",
					Entropy:     2.984234,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   58,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - our config file": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: filepath.Join(configPath, "simple.toml"),
			},
		},
		"ignore finding - doesn't match path": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.go",
			},
		},
		"detect finding - matches path,regex,entropy": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					File:        "tmp.py",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 22,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - allowlist regex": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
		},

		// Rule
		"rule - ignore path": {
			cfgName:      "valid/rule_path_only",
			baselinePath: ".baseline.json",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: ".baseline.json",
			},
		},
		"rule - detect path ": {
			cfgName: "valid/rule_path_only",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					Description: "Python Files",
					Match:       "file detected: tmp.py",
					File:        "tmp.py",
					RuleID:      "python-files-only",
					Tags:        []string{},
				},
			},
		},
		"rule - match based on entropy": {
			cfgName: "valid/rule_entropy_group",
			fragment: Fragment{
				Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "discord-api-key",
					Description: "Discord API key",
					File:        "tmp.go",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 7,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},

		// Allowlists
		"global allowlist - ignore regex": {
			cfgName: "valid/allowlist_global_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"global allowlist - detect, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw: `
const token = "mockSecret";
// const token = "changeit";`,
				FilePath: "config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "config.txt",
					Line:        "\nconst token = \"mockSecret\";",
					Match:       `token = "mockSecret"`,
					Secret:      "mockSecret",
					Entropy:     2.9219282,
					StartLine:   1,
					EndLine:     1,
					StartColumn: 8,
					EndColumn:   27,
					Tags:        []string{},
				},
			},
		},
		"global allowlist - ignore, matches all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `token := "mockSecret";`,
				FilePath: "node_modules/config.txt",
			},
		},
		"global allowlist - detect path, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `var token = "fakeSecret";`,
				FilePath: "node_modules/config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "node_modules/config.txt",
					Line:        "var token = \"fakeSecret\";",
					Match:       `token = "fakeSecret"`,
					Secret:      "fakeSecret",
					Entropy:     2.8464394,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 5,
					EndColumn:   24,
					Tags:        []string{},
				},
			},
		},
		"allowlist - ignore commit": {
			cfgName: "valid/allowlist_rule_commit",
			fragment: Fragment{
				Raw:       `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath:  "tmp.go",
				CommitSHA: "allowthiscommit",
			},
		},
		"allowlist - ignore path": {
			cfgName: "valid/allowlist_rule_path",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"allowlist - ignore path when extending": {
			cfgName: "valid/allowlist_rule_extend_default",
			fragment: Fragment{
				Raw:      `token = "aebfab88-7596-481d-82e8-c60c8f7de0c0"`,
				FilePath: "path/to/your/problematic/file.js",
			},
		},
		"allowlist - ignore regex": {
			cfgName: "valid/allowlist_rule_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"fragment level composite": {
			cfgName: "composite",
			fragment: Fragment{
				Raw: multili,
			},
			expectedFindings: []report.Finding{
				{
					Description: "Primary rule",
					RuleID:      "primary-rule",
					StartLine:   5,
					EndLine:     5,
					StartColumn: 5,
					EndColumn:   26,
					Line:        "\n\t\t\tpassword = \"secret123\"",
					Match:       `password = "secret123"`,
					Secret:      "secret123",
					Entropy:     2.9477028846740723,
					Tags:        []string{},
				},
			},
			expectedAuxOutput: "Required:    username-rule:1:admin\n",
		},
		// Decoding
		"detect encoded": {
			cfgName: "encoded",
			fragment: Fragment{
				Raw:      encodedTestValues,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{ // Plain text key captured by normal rule
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					RuleID:      "private-key",
					Tags:        []string{"key", "private"},
					StartLine:   2,
					EndLine:     5,
					StartColumn: 2,
					EndColumn:   26,
					Entropy:     5.350665,
				},
				{ // Encoded key captured by custom b64 regex rule
					Description: "Private Key",
					Secret:      "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					Match:       "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "b64-encoded-private-key",
					Tags:        []string{"key", "private"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.3861146,
				},
				{ // Encoded key captured by plain text rule using the decoder
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "private-key",
					Tags:        []string{"key", "private", "decoded:base64", "decode-depth:1"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.350665,
				},
				{ // Encoded Small secret at the end to make sure it's picked up by the decoding
					Description: "Small Secret",
					Secret:      "small-secret",
					Match:       "small-secret",
					File:        "tmp.go",
					Line:        "\nc21hbGwtc2VjcmV0",
					RuleID:      "small-secret",
					Tags:        []string{"small", "secret", "decoded:base64", "decode-depth:1"},
					StartLine:   15,
					EndLine:     15,
					StartColumn: 2,
					EndColumn:   17,
					Entropy:     3.0849626,
				},
				{ // Secret where the decoded match goes outside the encoded value
					Description: "Overlapping",
					Secret:      "decoded-secret-value00",
					Match:       "secret=decoded-secret-value00",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:base64", "decode-depth:1"},
					StartLine:   18,
					EndLine:     18,
					StartColumn: 2,
					EndColumn:   40,
					Entropy:     3.4428623,
				},
				{ // This just confirms that with no allowlist the pattern is detected (i.e. the regex is good)
			
[truncated at 20000 of 95196 bytes]
[evidence record sha256:1075cf33c6acfcf28cf5e9480c3ca4c01f81efe0d560dae31c750fadfc00d283 kind tool-call:read]
tool read <- {"path":"cmd/generate/config/base/config.go"}
tool read ok: package base

import (
	"fmt"
	"strings"

	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/regexp"
)

func CreateGlobalConfig() config.Config {
	return config.Config{
		Title: "gitleaks config",
		Allowlists: []*config.Allowlist{
			{
				Description: "global allow lists",
				Regexes: []*regexp.Regexp{
					// ----------- General placeholders -----------
					regexp.MustCompile(`(?i)^true|false|null$`),
					// Awkward workaround to detect repeated characters.
					func() *regexp.Regexp {
						var (
							letters  = "abcdefghijklmnopqrstuvwxyz*."
							patterns []string
						)
						for _, char := range letters {
							if char != '*' || char == '.' {
								patterns = append(patterns, fmt.Sprintf("\\%c+", char))
							} else {
								patterns = append(patterns, fmt.Sprintf("%c+", char))
							}
						}
						return regexp.MustCompile("^(?i:" + strings.Join(patterns, "|") + ")$")
					}(),

					// ----------- Environment Variables -----------
					regexp.MustCompile(`^\$(?:\d+|{\d+})$`),
					regexp.MustCompile(`^\$(?:[A-Z_]+|[a-z_]+)$`),
					regexp.MustCompile(`^\${(?:[A-Z_]+|[a-z_]+)}$`),

					// ----------- Interpolated Variables -----------
					// Ansible (https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html)
					regexp.MustCompile(`^\{\{[ \t]*[\w ().|]+[ \t]*}}$`),
					// GitHub Actions
					// https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables
					// https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions
					regexp.MustCompile(`^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$`),
					// NuGet (https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables)
					regexp.MustCompile(`^%(?:[A-Z_]+|[a-z_]+)%$`),
					// String formatting.
					regexp.MustCompile(`^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$`), // Golang (https://pkg.go.dev/fmt)
					regexp.MustCompile(`^\{\d{0,2}}$`),                       // Python (https://docs.python.org/3/tutorial/inputoutput.html)
					// Urban Code Deploy (https://www.ibm.com/support/pages/replace-token-step-replaces-replacement-values-windows-variables)
					regexp.MustCompile(`^@(?:[A-Z_]+|[a-z_]+)@$`),

					// ----------- Miscellaneous -----------
					// File paths
					regexp.MustCompile(`^/Users/(?i)[a-z0-9]+/[\w .-/]+$`),              // MacOS
					regexp.MustCompile(`^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$`), // Linux
					// 11980 Jps -Dapplication.home=D:\develop_tools\jdk\jdk1.8.0_131 -Xms8m
					//regexp.MustCompile(`^$`), // Windows
				},
				Paths: []*regexp.Regexp{
					regexp.MustCompile(`gitleaks\.toml`),

					// ----------- Documents and media -----------
					regexp.MustCompile(`(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$`), // Images
					regexp.MustCompile(`(?i)\.(?:eot|[ot]tf|woff2?)$`),           // Fonts
					regexp.MustCompile(`(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$`),

					// ----------- Golang files -----------
					regexp.MustCompile(`go\.(?:mod|sum|work(?:\.sum)?)$`),
					regexp.MustCompile(`(?:^|/)vendor/modules\.txt$`),
					regexp.MustCompile(`(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$`),

					// ----------- Java files -----------
					// Gradle
					regexp.MustCompile(`(?:^|/)gradlew(?:\.bat)?$`),
					regexp.MustCompile(`(?:^|/)gradle\.lockfile$`),
					regexp.MustCompile(`(?:^|/)mvnw(?:\.cmd)?$`),
					regexp.MustCompile(`(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$`),

					// ----------- JavaScript files -----------
					// Dependencies and lock files.
					regexp.MustCompile(`(?:^|/)node_modules(?:/.*)?$`),
					regexp.MustCompile(`(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$`),
					regexp.MustCompile(`(?:^|/)bower_components(?:/.*)?$`),
					// TODO: Add more common static assets, such as swagger-ui.
					regexp.MustCompile(`(?:^|/)(?:angular|bootstrap|jquery(?:-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(?:\.min)?\.js(?:\.map)?$`),
					regexp.MustCompile(`(?:^|/)javascript\.json$`),

					// ----------- Python files -----------
					// Dependencies and lock files.
					regexp.MustCompile(`(?:^|/)(?:Pipfile|poetry)\.lock$`),
					// Virtual environments
					regexp.MustCompile(`(?i)(?:^|/)(?:v?env|virtualenv)/lib(?:64)?(?:/.*)?$`),
					regexp.MustCompile(`(?i)(?:^|/)(?:lib(?:64)?/python[23](?:\.\d{1,2})+|python/[23](?:\.\d{1,2})+/lib(?:64)?)(?:/.*)?$`),
					// dist-info directory (https://py-pkgs.org/04-package-structure.html#building-sdists-and-wheels)
					regexp.MustCompile(`(?i)(?:^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(?:/.+)?$`),

					// ----------- Ruby files -----------
					regexp.MustCompile(`(?:^|/)vendor/(?:bundle|ruby)(?:/.*?)?$`),
					regexp.MustCompile(`\.gem$`), // tar archive

					// Misc
					regexp.MustCompile(`verification-metadata\.xml`),
					regexp.MustCompile(`Database.refactorlog`),

					// ----------- Git files ------------
					regexp.MustCompile(`(?:^|/)\.git$`),
				},
				StopWords: []string{
					"abcdefghijklmnopqrstuvwxyz", // character range
					// ----------- Secrets -----------
					// Checkmarx client secret. (https://github.com/checkmarx-ts/checkmarx-python-sdk/blob/86560f6e2a3e46d16322101294da10d5d190312d/README.md?plain=1#L56)
					"014df517-39d1-4453-b7b3-9930c563627c",
				},
			},
		},
	}
}

[evidence record sha256:7e432158e428a2b3e7eca5aa375605f06d568623e0a7a4198c7798d39a6e1d83 kind tool-call:read]
step 2: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect_test.go","maxBytes":60000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq env var, semicolon": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 21,
					EndColumn:   74,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq url": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-sensitive-url",
					Description: "Sidekiq Sensitive URL",
					File:        "tmp.sh",
					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
					Secret:      "cafeb4b3:d3adb33f",
					Entropy:     2.984234,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   58,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - our config file": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: filepath.Join(configPath, "simple.toml"),
			},
		},
		"ignore finding - doesn't match path": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.go",
			},
		},
		"detect finding - matches path,regex,entropy": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					File:        "tmp.py",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 22,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - allowlist regex": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
		},

		// Rule
		"rule - ignore path": {
			cfgName:      "valid/rule_path_only",
			baselinePath: ".baseline.json",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: ".baseline.json",
			},
		},
		"rule - detect path ": {
			cfgName: "valid/rule_path_only",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					Description: "Python Files",
					Match:       "file detected: tmp.py",
					File:        "tmp.py",
					RuleID:      "python-files-only",
					Tags:        []string{},
				},
			},
		},
		"rule - match based on entropy": {
			cfgName: "valid/rule_entropy_group",
			fragment: Fragment{
				Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "discord-api-key",
					Description: "Discord API key",
					File:        "tmp.go",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 7,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},

		// Allowlists
		"global allowlist - ignore regex": {
			cfgName: "valid/allowlist_global_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"global allowlist - detect, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw: `
const token = "mockSecret";
// const token = "changeit";`,
				FilePath: "config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "config.txt",
					Line:        "\nconst token = \"mockSecret\";",
					Match:       `token = "mockSecret"`,
					Secret:      "mockSecret",
					Entropy:     2.9219282,
					StartLine:   1,
					EndLine:     1,
					StartColumn: 8,
					EndColumn:   27,
					Tags:        []string{},
				},
			},
		},
		"global allowlist - ignore, matches all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `token := "mockSecret";`,
				FilePath: "node_modules/config.txt",
			},
		},
		"global allowlist - detect path, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `var token = "fakeSecret";`,
				FilePath: "node_modules/config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "node_modules/config.txt",
					Line:        "var token = \"fakeSecret\";",
					Match:       `token = "fakeSecret"`,
					Secret:      "fakeSecret",
					Entropy:     2.8464394,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 5,
					EndColumn:   24,
					Tags:        []string{},
				},
			},
		},
		"allowlist - ignore commit": {
			cfgName: "valid/allowlist_rule_commit",
			fragment: Fragment{
				Raw:       `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath:  "tmp.go",
				CommitSHA: "allowthiscommit",
			},
		},
		"allowlist - ignore path": {
			cfgName: "valid/allowlist_rule_path",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"allowlist - ignore path when extending": {
			cfgName: "valid/allowlist_rule_extend_default",
			fragment: Fragment{
				Raw:      `token = "aebfab88-7596-481d-82e8-c60c8f7de0c0"`,
				FilePath: "path/to/your/problematic/file.js",
			},
		},
		"allowlist - ignore regex": {
			cfgName: "valid/allowlist_rule_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"fragment level composite": {
			cfgName: "composite",
			fragment: Fragment{
				Raw: multili,
			},
			expectedFindings: []report.Finding{
				{
					Description: "Primary rule",
					RuleID:      "primary-rule",
					StartLine:   5,
					EndLine:     5,
					StartColumn: 5,
					EndColumn:   26,
					Line:        "\n\t\t\tpassword = \"secret123\"",
					Match:       `password = "secret123"`,
					Secret:      "secret123",
					Entropy:     2.9477028846740723,
					Tags:        []string{},
				},
			},
			expectedAuxOutput: "Required:    username-rule:1:admin\n",
		},
		// Decoding
		"detect encoded": {
			cfgName: "encoded",
			fragment: Fragment{
				Raw:      encodedTestValues,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{ // Plain text key captured by normal rule
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\n-----BEGIN PRIVATE KEY-----\n135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					RuleID:      "private-key",
					Tags:        []string{"key", "private"},
					StartLine:   2,
					EndLine:     5,
					StartColumn: 2,
					EndColumn:   26,
					Entropy:     5.350665,
				},
				{ // Encoded key captured by custom b64 regex rule
					Description: "Private Key",
					Secret:      "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					Match:       "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "b64-encoded-private-key",
					Tags:        []string{"key", "private"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.3861146,
				},
				{ // Encoded key captured by plain text rule using the decoder
					Description: "Private Key",
					Secret:      "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					Match:       "-----BEGIN PRIVATE KEY-----\n435f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb\nu+QDkg0spw==\n-----END PRIVATE KEY-----",
					File:        "tmp.go",
					Line:        "\nprivate_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'",
					RuleID:      "private-key",
					Tags:        []string{"key", "private", "decoded:base64", "decode-depth:1"},
					StartLine:   8,
					EndLine:     8,
					StartColumn: 16,
					EndColumn:   207,
					Entropy:     5.350665,
				},
				{ // Encoded Small secret at the end to make sure it's picked up by the decoding
					Description: "Small Secret",
					Secret:      "small-secret",
					Match:       "small-secret",
					File:        "tmp.go",
					Line:        "\nc21hbGwtc2VjcmV0",
					RuleID:      "small-secret",
					Tags:        []string{"small", "secret", "decoded:base64", "decode-depth:1"},
					StartLine:   15,
					EndLine:     15,
					StartColumn: 2,
					EndColumn:   17,
					Entropy:     3.0849626,
				},
				{ // Secret where the decoded match goes outside the encoded value
					Description: "Overlapping",
					Secret:      "decoded-secret-value00",
					Match:       "secret=decoded-secret-value00",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:base64", "decode-depth:1"},
					StartLine:   18,
					EndLine:     18,
					StartColumn: 2,
					EndColumn:   40,
					Entropy:     3.4428623,
				},
				{ // This just confirms that with no allowlist the pattern is detected (i.e. the regex is good)
					Description: "Make sure this would be detected with no allowlist",
					Secret:      "lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw",
					Match:       "password=\"lRqBK-z5kf4-please-ignore-me-X-XIJM2Pddw\"",
					File:        "tmp.go",
					Line:        "\npassword=\"bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw==\"",
					RuleID:      "decoded-password-dont-ignore",
					Tags:        []string{"decode-ignore", "decoded:base64", "decode-depth:1"},
					StartLine:   23,
					EndLine:     23,
					StartColumn: 2,
					EndColumn:   68,
					Entropy:     4.5841837,
				},
				{ // Hex encoded data check
					Description: "Overlapping",
					Secret:      "decoded-secret-valuevHEX",
					Match:       "secret=decoded-secret-valuevHEX",
					File:        "tmp.go",
					Line:        "\nsecret=6465636F6465642D7365637265742D76616C756576484558",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:hex", "decode-depth:1"},
					StartLine:   26,
					EndLine:     26,
					StartColumn: 2,
					EndColumn:   56,
					Entropy:     3.6531072,
				},
				{ // handle partial encoded percent data
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev2",
					Match:       "secret=decoded-secret-valuev2",
					File:        "tmp.go",
					Line:        "\nsecret=decoded-%73%65%63%72%65%74-valuev2",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decode-depth:1"},
					StartLine:   30,
					EndLine:     30,
					StartColumn: 2,
					EndColumn:   42,
					Entropy:     3.4428623,
				},
				{ // handle partial encoded percent data
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev3",
					Match:       "secret=decoded-secret-valuev3",
					File:        "tmp.go",
					Line:        "\nsecret=%64%65coded-%73%65%63%72%65%74-valuev3",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decode-depth:1"},
					StartLine:   32,
					EndLine:     32,
					StartColumn: 2,
					EndColumn:   46,
					Entropy:     3.4428623,
				},
				{ // Encoded AWS config with a access key id inside a JWT
					Description: "AWS IAM Unique Identifier",
					Secret:      "ASIAIOSFODNN7LXM10JI",
					Match:       " ASIAIOSFODNN7LXM10JI",
					File:        "tmp.go",
					Line:        "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
					RuleID:      "aws-iam-unique-identifier",
					Tags:        []string{"aws", "identifier", "decoded:base64", "decode-depth:2"},
					StartLine:   11,
					EndLine:     11,
					StartColumn: 39,
					EndColumn:   344,
					Entropy:     3.6841838,
				},
				{ // Encoded AWS config with a secret access key inside a JWT
					Description: "AWS Secret Access Key",
					Secret:      "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
					Match:       "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEFUCDlEX1A",
					File:        "tmp.go",
					Line:        "\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA",
					RuleID:      "aws-secret-access-key",
					Tags:        []string{"aws", "secret", "decoded:base64", "decode-depth:2"},
					StartLine:   11,
					EndLine:     11,
					StartColumn: 39,
					EndColumn:   344,
					Entropy:     4.721928,
				},
				{ // Secret where the decoded match goes outside the encoded value and then encoded again
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nc2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:base64", "decode-depth:2"},
					StartLine:   20,
					EndLine:     20,
					StartColumn: 2,
					EndColumn:   49,
					Entropy:     3.3037016,
				},
				{ // handle encodings that touch eachother
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev5",
					Match:       "secret=decoded-secret-valuev5",
					File:        "tmp.go",
					Line:        "\nsecret%3d6465636F6465642D7365637265742D76616C75657635",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:hex", "decode-depth:2"},
					StartLine:   40,
					EndLine:     40,
					StartColumn: 2,
					EndColumn:   54,
					Entropy:     3.4428623,
				},
				{ // handle partial encoded percent data465642D7365637265742D76616C75657635
					Description: "Overlapping",
					Secret:      "decoded-secret-valuev4",
					Match:       "secret=decoded-secret-valuev4",
					File:        "tmp.go",
					Line:        "\nc2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:3"},
					StartLine:   38,
					EndLine:     38,
					StartColumn: 2,
					EndColumn:   55,
					Entropy:     3.4428623,
				},
				{ // multiple percent encodings in a single layer base64
					Description: "Overlapping",
					Secret:      "decoded-secret-valuex86",
					Match:       "secret=decoded-secret-valuex86",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"},
					StartLine:   42,
					EndLine:     42,
					StartColumn: 2,
					EndColumn:   44,
					Entropy:     3.6381476,
				},
				{ // base64 encoded partially percent encoded value
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:2"},
					StartLine:   44,
					EndLine:     44,
					StartColumn: 2,
					EndColumn:   52,
					Entropy:     3.3037016,
				},
				{ // one of the lines above that went through... a lot
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nLook at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:hex", "decoded:base64", "decode-depth:7"},
					StartLine:   47,
					EndLine:     47,
					StartColumn: 22,
					EndColumn:   177,
					Entropy:     3.3037016,
				},
				{ // Multi percent encode two random characters close to the bounds of the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:5"},
					StartLine:   50,
					EndLine:     50,
					StartColumn: 2,
					EndColumn:   300,
					Entropy:     3.3037016,
				},
				{ // The similar to the above but also touching the edge of the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"},
					StartLine:   52,
					EndLine:     52,
					StartColumn: 2,
					EndColumn:   86,
					Entropy:     3.3037016,
				},
				{ // The similar to the above but also touching and overlapping the base64
					Description: "Overlapping",
					Secret:      "decoded-secret-value",
					Match:       "secret=decoded-secret-value",
					File:        "tmp.go",
					Line:        "\nsecret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34",
					RuleID:      "overlapping",
					Tags:        []string{"overlapping", "decoded:percent", "decoded:base64", "decode-depth:4"},
					StartLine:   54,
					EndLine:     54,
					StartColumn: 2,
					EndColumn:   88,
					Entropy:     3.3037016,
				},
			},
		},
	}

	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			viper.Reset()
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)
			cfg, err := vc.Translate()
			cfg.Path = filepath.Join(configPath, tt.cfgName+".toml")
			assert.Equal(t, tt.wantError, err)
			d := NewDetector(cfg)
			d.MaxDecodeDepth = maxDecodeDepth
			d.baselinePath = tt.baselinePath

			findings := d.Detect(tt.fragment)

			compare(t, findings, tt.expectedFindings)

			// extremely goofy way to test auxiliary findings
			// capture stdout and print that sonabitch
			// TODO
			if tt.expectedAuxOutput != "" {
				capturedOutput := captureStdout(func() {
					for _, finding := range findings {
						finding.PrintRequiredFindings()
					}
				})

				// Clean up the output for comparison (remove ANSI color codes)
				cleanOutput := stripANSI(capturedOutput)
				expectedClean := stripANSI(tt.expectedAuxOutput)

				assert.Equal(t, expectedClean, cleanOutput, "Auxiliary output should match")
			}

		})
	}
}

func stripANSI(s string) string {
	ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
	return ansiRegex.ReplaceAllString(s, "")
}

func captureStdout(f func()) string {
	oldStdout := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w

	f()

	w.Close()
	os.Stdout = oldStdout

	var buf bytes.Buffer
	io.Copy(&buf, r)
	return buf.String()
}

// TestFromGit tests the FromGit function
func TestFromGit(t *testing.T) {
	// TODO: Fix this test on windows.
	if runtime.GOOS == "windows" {
		t.Skipf("TODO: this fails on Windows: [git] fatal: bad object refs/remotes/origin/main?")
		return
	}
	tests := []struct {
		cfgName          string
		source           string
		logOpts          string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "small"),
			cfgName: "simple", // the remote url is `git@github.com:gitleaks/test.git`
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 19,
					EndColumn:   38,
					Line:        "\n    awsToken := \"AKIALALEMEL33243OLIA\"",
					Secret:      "AKIALALEMEL33243OLIA",
					Match:       "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					File:        "main.go",
					Date:        "2021-11-02T23:37:53Z",
					Commit:      "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587",
					Author:      "Zachary Rice",
					Email:       "zricer@protonmail.com",
					Message:     "Accidentally add a secret",
					Tags:        []string{"key", "AWS"},
					Fingerprint: "1b6da43b82b22e4eaa10bcf8ee591e91abbfc587:main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/1b6da43b82b22e4eaa10bcf8ee591e91abbfc587/main.go#L20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   9,
					EndLine:     9,
					StartColumn: 17,
					EndColumn:   36,
					Secret:      "AKIALALEMEL33243OLIA",
					Match:       "AKIALALEMEL33243OLIA",
					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
					File:        "foo/foo.go",
					Date:        "2021-11-02T23:48:06Z",
					Commit:      "491504d5a31946ce75e22554cc34203d8e5ff3ca",
					Author:      "Zach Rice",
					Email:       "zricer@protonmail.com",
					Message:     "adding foo package with secret",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
					Link:        "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "small"),
			logOpts: "--all foo...",
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   9,
					EndLine:     9,
					StartColumn: 17,
					EndColumn:   36,
					Secret:      "AKIALALEMEL33243OLIA",
					Line:        "\n\taws_token := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Date:        "2021-11-02T23:48:06Z",
					File:        "foo/foo.go",
					Commit:      "491504d5a31946ce75e22554cc34203d8e5ff3ca",
					Author:      "Zach Rice",
					Email:       "zricer@protonmail.com",
					Message:     "adding foo package with secret",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "491504d5a31946ce75e22554cc34203d8e5ff3ca:foo/foo.go:aws-access-key:9",
					Link:        "https://github.com/gitleaks/test/blob/491504d5a31946ce75e22554cc34203d8e5ff3ca/foo/foo.go#L9",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "archives"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "main.go.zst",
					Commit:      "db8789716fc664dbce0ed2d492570e92abf717a5",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:10:39Z",
					Message:     "Add main.go.zst",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "db8789716fc664dbce0ed2d492570e92abf717a5:main.go.zst:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/db8789716fc664dbce0ed2d492570e92abf717a5/main.go.zst#L20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.zip!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.zip!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.zip!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.7z!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.7z!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.7z!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.zst!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.zst!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.zst!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.xz",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.xz:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.zst",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.zst:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files/main.go.gz",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files/main.go.gz:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.xz!files/api.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "nested.tar.gz!archives/files.tar.xz!files/main.go",
					Commit:      "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68",
					Author:      "Test User",
					Email:       "user@example.com",
					Date:        "2025-05-27T05:08:50Z",
					Message:     "Add nested.tar.gz",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "07d2bd71800f1abf0421abe9bc4a83a6fdca1f68:nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
					Link:        "https://github.com/gitleaks/test/blob/07d2bd71800f1abf0421abe9bc4a83a6fdca1f68/nested.tar.gz",
				},
			},
		},
	}

	moveDotGit(t, "dotGit", ".git")
	defer moveDotGit(t, ".git", "dotGit")

	for _, tt := range tests {
		t.Run(strings.Join([]string{tt.cfgName, tt.source, tt.logOpts}, "/"), func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName("simple")
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)
			cfg, err := vc.Translate()
			require.NoError(t, err)
			detector := NewDetector(cfg)
			detector.MaxArchiveDepth = 8

			var ignorePath string
			info, err := os.Stat(tt.source)
			require.NoError(t, err)

			if info.IsDir() {
				ignorePath = filepath.Join(tt.source, ".gitleaksignore")
			} else {
				ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
			}
			err = detector.AddGitleaksIgnore(ignorePath)
			require.NoError(t, err)

			gitCmd, err := sources.NewGitLogCmd(tt.source, tt.logOpts)
			require.NoError(t, err)

			remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
			findings, err := detector.DetectGit(gitCmd, remote)
			require.NoError(t, err)

			for _, f := range findings {
				f.Match = "" // remove lines cause copying and pasting them has some wack formatting
			}
			assert.ElementsMatch(t, tt.expectedFindings, findings)
		})
	}
}

func TestFromGitStaged(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		logOpts          string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "staged"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   7,
					EndLine:     7,
					StartColumn: 18,
					EndColumn:   37,
					Line:        "\n\taws_token2 := \"AKIALALEMEL33243OLIA\" // this one is not",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "api/api.go",
					SymlinkFile: "",
					Commit:      "",
					Entropy:     3.0841837,
					Author:      "",
					Email:       "",
					Date:        "0001-01-01T00:00:00Z",
					Message:     "",
					Tags: []string{
						"key",
						"AWS",
					},
					Fingerprint: "api/api.go:aws-access-key:7",
					Link:        "",
				},
			},
		},
	}

	moveDotGit(t, "dotGit", ".git")
	defer moveDotGit(t, ".git", "dotGit")
	for _, tt := range tests {

		viper.AddConfigPath(configPath)
		viper.SetConfigName("simple")
		viper.SetConfigType("toml")
		err := viper.ReadInConfig()
		require.NoError(t, err)

		var vc config.ViperConfig
		err = viper.Unmarshal(&vc)
		require.NoError(t, err)
		cfg, err := vc.Translate()
		require.NoError(t, err)
		detector := NewDetector(cfg)
		err = detector.AddGitleaksIgnore(filepath.Join(tt.source, ".gitleaksignore"))
		require.NoError(t, err)
		gitCmd, err := sources.NewGitDiffCmd(tt.source, true)
		require.NoError(t, err)
		remote := NewRemoteInfo(scm.UnknownPlatform, tt.source)
		findings, err := detector.DetectGit(gitCmd, remote)
		require.NoError(t, err)

		for _, f := range findings {
			f.Match = "" // remove lines cause copying and pasting them has some wack formatting
		}
		assert.ElementsMatch(t, tt.expectedFindings, findings)
	}
}

// TestFromFiles tests the FromFiles function
func TestFromFiles(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "nogit"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/repos/nogit/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(repoBasePath, "nogit", "main.go"),
			cfgName: "simple",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/repos/nogit/main.go",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/repos/nogit/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:           filepath.Join(repoBasePath, "nogit", "api.go"),
			cfgName:          "simple",
			expectedFindings: []report.Finding{},
		},
		{
			source:  filepath.Join(repoBasePath, "nogit", ".env.prod"),
			cfgName: "generic",
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					StartLine:   4,
					EndLine:     4,
					StartColumn: 5,
					EndColumn:   35,
					Line:        "\nDB_PASSWORD=8ae31cacf141669ddfb5da",
					Match:       "PASSWORD=8ae31cacf141669ddfb5da",
					Secret:      "8ae31cacf141669ddfb5da",
					File:        "../testdata/repos/nogit/.env.prod",
					Tags:        []string{},
					Entropy:     3.5383105,
					Fingerprint: "../testdata/repos/nogit/.env.prod:generic-api-key:4",
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			cfg, _ := vc.Translate()
			detector := NewDetector(cfg)

			info, err := os.Stat(tt.source)
			require.NoError(t, err)

			var ignorePath string
			if info.IsDir() {
				ignorePath = filepath.Join(tt.source, ".gitleaksignore")
			} else {
				ignorePath = filepath.Join(filepath.Dir(tt.source), ".gitleaksignore")
			}
			err = detector.AddGitleaksIgnore(ignorePath)
			require.NoError(t, err)

			detector.FollowSymlinks = true
			paths, err := sources.DirectoryTargets(tt.source, detector.Sema, true, cfg.Allowlists)
			require.NoError(t, err)

			findings, err := detector.DetectFiles(paths)
			require.NoError(t, err)

			// TODO: Temporary mitigation.
			// https://github.com/gitleaks/gitleaks/issues/1641
			normalizedFindings := make([]report.Finding, len(findings))
			for i, f := range findings {
				if strings.HasSuffix(f.Line, "\r") {
					f.Line = strings.ReplaceAll(f.Line, "\r", "")
				}
				if strings.HasSuffix(f.Match, "\r") {
					f.EndColumn = f.EndColumn - 1
					f.Match = strings.ReplaceAll(f.Match, "\r", "")
				}
				normalizedFindings[i] = f
			}
			assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
		})
	}
}

func TestDetectWithArchives(t *testing.T) {
	tests := []struct {
		cfgName          string
		source           string
		expireContext    bool
		expectedFindings []report.Finding
	}{
		{
			source:           filepath.Join(archivesBasePath, "this-path-does-not-exist"),
			cfgName:          "archives",
			expectedFindings: []report.Finding{},
		},
		{
			source:  filepath.Join(archivesBasePath, "files"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.gz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.gz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.xz",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.xz:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files/main.go.zst",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files/main.go.zst:aws-access-key:20",
				},
			},
		},
		{
			source:  filepath.Join(archivesBasePath, "files.7z"),
			cfgName: "archives",
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/files.7z!files/api.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/files.7z!files/api.go:aws-access-key:20",
				},
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					StartLine:   20,
					EndLine:     20,
					StartColumn: 16,
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALA
[truncated at 60000 of 95196 bytes]
[evidence record sha256:22d73bb3a5615881d7db74f647b77e3c3553693555142e6ba92d193d3f3f4b3d kind tool-call:read]
step 3: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect_test.go","maxBytes":15000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq env var, semicolon": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 21,
					EndColumn:   74,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq url": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-sensitive-url",
					Description: "Sidekiq Sensitive URL",
					File:        "tmp.sh",
					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
					Secret:      "cafeb4b3:d3adb33f",
					Entropy:     2.984234,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   58,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - our config file": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: filepath.Join(configPath, "simple.toml"),
			},
		},
		"ignore finding - doesn't match path": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.go",
			},
		},
		"detect finding - matches path,regex,entropy": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					File:        "tmp.py",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 22,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - allowlist regex": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
		},

		// Rule
		"rule - ignore path": {
			cfgName:      "valid/rule_path_only",
			baselinePath: ".baseline.json",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: ".baseline.json",
			},
		},
		"rule - detect path ": {
			cfgName: "valid/rule_path_only",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					Description: "Python Files",
					Match:       "file detected: tmp.py",
					File:        "tmp.py",
					RuleID:      "python-files-only",
					Tags:        []string{},
				},
			},
		},
		"rule - match based on entropy": {
			cfgName: "valid/rule_entropy_group",
			fragment: Fragment{
				Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "discord-api-key",
					Description: "Discord API key",
					File:        "tmp.go",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 7,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},

		// Allowlists
		"global allowlist - ignore regex": {
			cfgName: "valid/allowlist_global_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"global allowlist - detect, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw: `
const token = "mockSecret";
// const token = "changeit";`,
				FilePath: "config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "config.txt",
					Line:        "\nconst token = \"mockSecret\";",
					Match:       `token = "mockSecret"`,
					Secret:      "mockSecret",
					Entropy:     2.9219282,
					StartLine:   1,
					EndLine:     1,
					StartColumn: 8,
					EndColumn:   27,
					Tags:        []string{},
				},
			},
		},
		"global allowlist - ignore, matches all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `token := "mockSecret";`,
				FilePath: "node_modules/config.txt",
			},
		},
		"global allowlist - detect path, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `var token = "fakeSecret";`,
				FilePath: "node_modules/config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "node_modules/config.txt",
					Line:        "var token = \"fakeSecret\";",
					Match:       `token = "fakeSecret"`,
					Secret:      "fakeSecret",
					Entropy:     2.8464394,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 5,
					EndColumn:   24,
					Tags:        []string{},
				},
			},
		},
		"allowlist - ignore commit": {
			cfgName: "valid/allowlist_rule_commit",
			fragment: Fragment{
				Raw:       `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath:  "tmp.go",
				CommitSHA: "allowthiscommit",
			},
		},
		"allowlist - ignore path": {
			cfgName: "valid/allowlist_rule_path",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"allowlist - ignore path when extending": {
			cfg
[truncated at 15000 of 95196 bytes]
[evidence record sha256:c178db78c9fefbcdd5f1a34ceb079e1b7866c28418ed6846837a54a85d2b5d4f kind tool-call:read]
step 4: calling local:qwen3.6:35b-a3b
tool read <- {"path":"cmd/generate/config/base/config.go","maxBytes":4000}
tool read ok: package base

import (
	"fmt"
	"strings"

	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/regexp"
)

func CreateGlobalConfig() config.Config {
	return config.Config{
		Title: "gitleaks config",
		Allowlists: []*config.Allowlist{
			{
				Description: "global allow lists",
				Regexes: []*regexp.Regexp{
					// ----------- General placeholders -----------
					regexp.MustCompile(`(?i)^true|false|null$`),
					// Awkward workaround to detect repeated characters.
					func() *regexp.Regexp {
						var (
							letters  = "abcdefghijklmnopqrstuvwxyz*."
							patterns []string
						)
						for _, char := range letters {
							if char != '*' || char == '.' {
								patterns = append(patterns, fmt.Sprintf("\\%c+", char))
							} else {
								patterns = append(patterns, fmt.Sprintf("%c+", char))
							}
						}
						return regexp.MustCompile("^(?i:" + strings.Join(patterns, "|") + ")$")
					}(),

					// ----------- Environment Variables -----------
					regexp.MustCompile(`^\$(?:\d+|{\d+})$`),
					regexp.MustCompile(`^\$(?:[A-Z_]+|[a-z_]+)$`),
					regexp.MustCompile(`^\${(?:[A-Z_]+|[a-z_]+)}$`),

					// ----------- Interpolated Variables -----------
					// Ansible (https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html)
					regexp.MustCompile(`^\{\{[ \t]*[\w ().|]+[ \t]*}}$`),
					// GitHub Actions
					// https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables
					// https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions
					regexp.MustCompile(`^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$`),
					// NuGet (https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables)
					regexp.MustCompile(`^%(?:[A-Z_]+|[a-z_]+)%$`),
					// String formatting.
					regexp.MustCompile(`^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$`), // Golang (https://pkg.go.dev/fmt)
					regexp.MustCompile(`^\{\d{0,2}}$`),                       // Python (https://docs.python.org/3/tutorial/inputoutput.html)
					// Urban Code Deploy (https://www.ibm.com/support/pages/replace-token-step-replaces-replacement-values-windows-variables)
					regexp.MustCompile(`^@(?:[A-Z_]+|[a-z_]+)@$`),

					// ----------- Miscellaneous -----------
					// File paths
					regexp.MustCompile(`^/Users/(?i)[a-z0-9]+/[\w .-/]+$`),              // MacOS
					regexp.MustCompile(`^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$`), // Linux
					// 11980 Jps -Dapplication.home=D:\develop_tools\jdk\jdk1.8.0_131 -Xms8m
					//regexp.MustCompile(`^$`), // Windows
				},
				Paths: []*regexp.Regexp{
					regexp.MustCompile(`gitleaks\.toml`),

					// ----------- Documents and media -----------
					regexp.MustCompile(`(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$`), // Images
					regexp.MustCompile(`(?i)\.(?:eot|[ot]tf|woff2?)$`),           // Fonts
					regexp.MustCompile(`(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$`),

					// ----------- Golang files -----------
					regexp.MustCompile(`go\.(?:mod|sum|work(?:\.sum)?)$`),
					regexp.MustCompile(`(?:^|/)vendor/modules\.txt$`),
					regexp.MustCompile(`(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$`),

					// ----------- Java files -----------
					// Gradle
					regexp.MustCompile(`(?:^|/)gradlew(?:\.bat)?$`),
					regexp.MustCompile(`(?:^|/)gradle\.lockfile$`),
					regexp.MustCompile(`(?:^|/)mvnw(?:\.cmd)?$`),
					regexp.MustCompile(`(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$`),

					// ----------- JavaScript files -----------
					// Dependencies and lock files.
					regexp.MustCompile(`(?:^|/)node_modules(?:/.*)?$`),
					regexp.MustCompile(`(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$`),
					regexp.MustCompile(`(?:^|/)bower_components(?:/.*)?
[truncated at 4000 of 5530 bytes]
[evidence record sha256:f45280e13d9ce7fa8c58a1c7a254dec6c5226e2cbe74527e11fbb7a05c1827a8 kind tool-call:read]
tool shell <- {"command":"gofmt -l detect/ cmd/ config/ report/ sources/ regexp/ logging/ version/ main.go 2>/dev/null | head -30"}
tool shell failed: denied: /dev/null resolves outside the workspace /work
[evidence record sha256:1425d5f01082bf862d6af7c8cd58ff8e57cae46290bbef957a70508635d7d031 kind tool-call:shell]
step 5: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"ls -la .campaign/gomod/"}
tool shell ok: exit code: 0
stdout:
total 0
drwxr-xr-x  9 root root  288 Sep  4 06:08 .
drwxr-xr-x  4 root root  128 Sep  4 06:08 ..
drwxr-xr-x  3 root root   96 Sep  4 06:08 cache
drwxr-xr-x  3 root root   96 Sep  4 06:08 dario.cat
drwxr-xr-x 45 root root 1440 Sep  4 06:08 github.com
drwxr-xr-x  3 root root   96 Sep  4 06:08 go.uber.org
drwxr-xr-x 34 root root 1088 Sep  4 06:08 go4.org@v0.0.0-20230225012048-214862532bf5
drwxr-xr-x  3 root root   96 Sep  4 06:08 golang.org
drwxr-xr-x  4 root root  128 Sep  4 06:08 gopkg.in
[evidence record sha256:2987a916bbdc17988042983c618b80afd7d6fb1b9dc12a23207bdb1b1b76df8e kind tool-call:shell]
step 6: calling local:qwen3.6:35b-a3b
tool search <- {"pattern":"context\\.WithCancel","path":"detect/detect_test.go"}
tool search failed: failed: ENOTDIR: not a directory, scandir '/work/detect/detect_test.go', caused by ENOTDIR
[evidence record sha256:626bf03b5e744260ad86e88219f66d77212897304f28e29dbe3623b6130587ca kind tool-call:search]
step 7: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect_test.go","maxBytes":15000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq env var, semicolon": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 21,
					EndColumn:   74,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq url": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-sensitive-url",
					Description: "Sidekiq Sensitive URL",
					File:        "tmp.sh",
					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
					Secret:      "cafeb4b3:d3adb33f",
					Entropy:     2.984234,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   58,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - our config file": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: filepath.Join(configPath, "simple.toml"),
			},
		},
		"ignore finding - doesn't match path": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.go",
			},
		},
		"detect finding - matches path,regex,entropy": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					File:        "tmp.py",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 22,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - allowlist regex": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
		},

		// Rule
		"rule - ignore path": {
			cfgName:      "valid/rule_path_only",
			baselinePath: ".baseline.json",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: ".baseline.json",
			},
		},
		"rule - detect path ": {
			cfgName: "valid/rule_path_only",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					Description: "Python Files",
					Match:       "file detected: tmp.py",
					File:        "tmp.py",
					RuleID:      "python-files-only",
					Tags:        []string{},
				},
			},
		},
		"rule - match based on entropy": {
			cfgName: "valid/rule_entropy_group",
			fragment: Fragment{
				Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "discord-api-key",
					Description: "Discord API key",
					File:        "tmp.go",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 7,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},

		// Allowlists
		"global allowlist - ignore regex": {
			cfgName: "valid/allowlist_global_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"global allowlist - detect, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw: `
const token = "mockSecret";
// const token = "changeit";`,
				FilePath: "config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "config.txt",
					Line:        "\nconst token = \"mockSecret\";",
					Match:       `token = "mockSecret"`,
					Secret:      "mockSecret",
					Entropy:     2.9219282,
					StartLine:   1,
					EndLine:     1,
					StartColumn: 8,
					EndColumn:   27,
					Tags:        []string{},
				},
			},
		},
		"global allowlist - ignore, matches all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `token := "mockSecret";`,
				FilePath: "node_modules/config.txt",
			},
		},
		"global allowlist - detect path, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `var token = "fakeSecret";`,
				FilePath: "node_modules/config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "node_modules/config.txt",
					Line:        "var token = \"fakeSecret\";",
					Match:       `token = "fakeSecret"`,
					Secret:      "fakeSecret",
					Entropy:     2.8464394,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 5,
					EndColumn:   24,
					Tags:        []string{},
				},
			},
		},
		"allowlist - ignore commit": {
			cfgName: "valid/allowlist_rule_commit",
			fragment: Fragment{
				Raw:       `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath:  "tmp.go",
				CommitSHA: "allowthiscommit",
			},
		},
		"allowlist - ignore path": {
			cfgName: "valid/allowlist_rule_path",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"allowlist - ignore path when extending": {
			cfg
[truncated at 15000 of 95196 bytes]
[evidence record sha256:84eacb8ef86bb6e32cde212e1c22e0fa2a24573f92d35c761d8b18dccce2bf0c kind tool-call:read]
tool shell <- {"command":"head -n 2130 detect/detect_test.go | tail -n 70"}
tool shell ok: exit code: 0
stdout:
			},
		},
		{
			source:           filepath.Join(archivesBasePath, "nested.tar.gz"),
			cfgName:          "archives",
			expireContext:    true,
			expectedFindings: []report.Finding{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			ctx, cancel := context.WithCancel(context.Background())
			if tt.expireContext {
				cancel()
			}

			cfg, _ := vc.Translate()
			detector := NewDetectorContext(ctx, cfg)
			detector.MaxArchiveDepth = 8

			findings, err := detector.DetectSource(
				ctx, &sources.Files{
					Path:            tt.source,
					Sema:            detector.Sema,
					Config:          &cfg,
					MaxArchiveDepth: detector.MaxArchiveDepth,
				},
			)

			if tt.expireContext {
				require.EqualError(t, err, "context canceled")
			} else {
				cancel()
				require.NoError(t, err)
			}

			// TODO: Temporary mitigation.
			// https://github.com/gitleaks/gitleaks/issues/1641
			normalizedFindings := make([]report.Finding, len(findings))
			for i, f := range findings {
				if strings.HasSuffix(f.Line, "\r") {
					f.Line = strings.ReplaceAll(f.Line, "\r", "")
				}
				if strings.HasSuffix(f.Match, "\r") {
					f.EndColumn = f.EndColumn - 1
					f.Match = strings.ReplaceAll(f.Match, "\r", "")
				}
				normalizedFindings[i] = f
			}
			assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
		})
	}

}

func TestDetectWithSymlinks(t *testing.T) {
	// TODO: Fix this test on windows.
	if runtime.GOOS == "windows" {
		t.Skipf("TODO: this returns no results on windows, I'm not sure why.")
[evidence record sha256:3b5241c7325fda08c8b25b86e13ded7a08e82139a4709907e517fbe0c2ca155e kind tool-call:shell]
tool read <- {"path":"detect/detect_test.go","maxBytes":15000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq env var, semicolon": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `echo hello1; export BUNDLE_ENTERPRISE__CONTRIBSYS__COM="cafebabe:deadbeef" && echo hello2`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=\"cafebabe:deadbeef\"",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 21,
					EndColumn:   74,
					Tags:        []string{},
				},
			},
		},
		"detect finding - sidekiq url": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-sensitive-url",
					Description: "Sidekiq Sensitive URL",
					File:        "tmp.sh",
					Line:        `url = "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:80/path?param1=true&param2=false#heading1"`,
					Match:       "http://cafeb4b3:d3adb33f@enterprise.contribsys.com:",
					Secret:      "cafeb4b3:d3adb33f",
					Entropy:     2.984234,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   58,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - our config file": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: filepath.Join(configPath, "simple.toml"),
			},
		},
		"ignore finding - doesn't match path": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.go",
			},
		},
		"detect finding - matches path,regex,entropy": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "generic-api-key",
					Description: "Generic API Key",
					File:        "tmp.py",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 22,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},
		"ignore finding - allowlist regex": {
			cfgName: "generic_with_py_path",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "load2523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
		},

		// Rule
		"rule - ignore path": {
			cfgName:      "valid/rule_path_only",
			baselinePath: ".baseline.json",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: ".baseline.json",
			},
		},
		"rule - detect path ": {
			cfgName: "valid/rule_path_only",
			fragment: Fragment{
				Raw:      `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
				FilePath: "tmp.py",
			},
			expectedFindings: []report.Finding{
				{
					Description: "Python Files",
					Match:       "file detected: tmp.py",
					File:        "tmp.py",
					RuleID:      "python-files-only",
					Tags:        []string{},
				},
			},
		},
		"rule - match based on entropy": {
			cfgName: "valid/rule_entropy_group",
			fragment: Fragment{
				Raw: `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"
//const Discord_Public_Key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "discord-api-key",
					Description: "Discord API key",
					File:        "tmp.go",
					Line:        `const Discord_Public_Key = "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5"`,
					Match:       "Discord_Public_Key = \"e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5\"",
					Secret:      "e7322523fb86ed64c836a979cf8465fbd436378c653c1db38f9ae87bc62a6fd5",
					Entropy:     3.7906237,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 7,
					EndColumn:   93,
					Tags:        []string{},
				},
			},
		},

		// Allowlists
		"global allowlist - ignore regex": {
			cfgName: "valid/allowlist_global_regex",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"global allowlist - detect, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw: `
const token = "mockSecret";
// const token = "changeit";`,
				FilePath: "config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "config.txt",
					Line:        "\nconst token = \"mockSecret\";",
					Match:       `token = "mockSecret"`,
					Secret:      "mockSecret",
					Entropy:     2.9219282,
					StartLine:   1,
					EndLine:     1,
					StartColumn: 8,
					EndColumn:   27,
					Tags:        []string{},
				},
			},
		},
		"global allowlist - ignore, matches all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `token := "mockSecret";`,
				FilePath: "node_modules/config.txt",
			},
		},
		"global allowlist - detect path, doesn't match all conditions": {
			cfgName: "valid/allowlist_global_multiple",
			fragment: Fragment{
				Raw:      `var token = "fakeSecret";`,
				FilePath: "node_modules/config.txt",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "test",
					File:        "node_modules/config.txt",
					Line:        "var token = \"fakeSecret\";",
					Match:       `token = "fakeSecret"`,
					Secret:      "fakeSecret",
					Entropy:     2.8464394,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 5,
					EndColumn:   24,
					Tags:        []string{},
				},
			},
		},
		"allowlist - ignore commit": {
			cfgName: "valid/allowlist_rule_commit",
			fragment: Fragment{
				Raw:       `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath:  "tmp.go",
				CommitSHA: "allowthiscommit",
			},
		},
		"allowlist - ignore path": {
			cfgName: "valid/allowlist_rule_path",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
		},
		"allowlist - ignore path when extending": {
			cfg
[truncated at 15000 of 95196 bytes]
[evidence record sha256:608c5d32f4990613dc9f19ee913333b95f2339898ac5e08c4864a65e34680df0 kind tool-call:read]
step 8: calling local:qwen3.6:35b-a3b
tool read <- {"path":"cmd/generate/config/base/config.go"}
tool read ok: package base

import (
	"fmt"
	"strings"

	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/regexp"
)

func CreateGlobalConfig() config.Config {
	return config.Config{
		Title: "gitleaks config",
		Allowlists: []*config.Allowlist{
			{
				Description: "global allow lists",
				Regexes: []*regexp.Regexp{
					// ----------- General placeholders -----------
					regexp.MustCompile(`(?i)^true|false|null$`),
					// Awkward workaround to detect repeated characters.
					func() *regexp.Regexp {
						var (
							letters  = "abcdefghijklmnopqrstuvwxyz*."
							patterns []string
						)
						for _, char := range letters {
							if char != '*' || char == '.' {
								patterns = append(patterns, fmt.Sprintf("\\%c+", char))
							} else {
								patterns = append(patterns, fmt.Sprintf("%c+", char))
							}
						}
						return regexp.MustCompile("^(?i:" + strings.Join(patterns, "|") + ")$")
					}(),

					// ----------- Environment Variables -----------
					regexp.MustCompile(`^\$(?:\d+|{\d+})$`),
					regexp.MustCompile(`^\$(?:[A-Z_]+|[a-z_]+)$`),
					regexp.MustCompile(`^\${(?:[A-Z_]+|[a-z_]+)}$`),

					// ----------- Interpolated Variables -----------
					// Ansible (https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html)
					regexp.MustCompile(`^\{\{[ \t]*[\w ().|]+[ \t]*}}$`),
					// GitHub Actions
					// https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables
					// https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions
					regexp.MustCompile(`^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$`),
					// NuGet (https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables)
					regexp.MustCompile(`^%(?:[A-Z_]+|[a-z_]+)%$`),
					// String formatting.
					regexp.MustCompile(`^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$`), // Golang (https://pkg.go.dev/fmt)
					regexp.MustCompile(`^\{\d{0,2}}$`),                       // Python (https://docs.python.org/3/tutorial/inputoutput.html)
					// Urban Code Deploy (https://www.ibm.com/support/pages/replace-token-step-replaces-replacement-values-windows-variables)
					regexp.MustCompile(`^@(?:[A-Z_]+|[a-z_]+)@$`),

					// ----------- Miscellaneous -----------
					// File paths
					regexp.MustCompile(`^/Users/(?i)[a-z0-9]+/[\w .-/]+$`),              // MacOS
					regexp.MustCompile(`^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$`), // Linux
					// 11980 Jps -Dapplication.home=D:\develop_tools\jdk\jdk1.8.0_131 -Xms8m
					//regexp.MustCompile(`^$`), // Windows
				},
				Paths: []*regexp.Regexp{
					regexp.MustCompile(`gitleaks\.toml`),

					// ----------- Documents and media -----------
					regexp.MustCompile(`(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$`), // Images
					regexp.MustCompile(`(?i)\.(?:eot|[ot]tf|woff2?)$`),           // Fonts
					regexp.MustCompile(`(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$`),

					// ----------- Golang files -----------
					regexp.MustCompile(`go\.(?:mod|sum|work(?:\.sum)?)$`),
					regexp.MustCompile(`(?:^|/)vendor/modules\.txt$`),
					regexp.MustCompile(`(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$`),

					// ----------- Java files -----------
					// Gradle
					regexp.MustCompile(`(?:^|/)gradlew(?:\.bat)?$`),
					regexp.MustCompile(`(?:^|/)gradle\.lockfile$`),
					regexp.MustCompile(`(?:^|/)mvnw(?:\.cmd)?$`),
					regexp.MustCompile(`(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$`),

					// ----------- JavaScript files -----------
					// Dependencies and lock files.
					regexp.MustCompile(`(?:^|/)node_modules(?:/.*)?$`),
					regexp.MustCompile(`(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$`),
					regexp.MustCompile(`(?:^|/)bower_components(?:/.*)?$`),
					// TODO: Add more common static assets, such as swagger-ui.
					regexp.MustCompile(`(?:^|/)(?:angular|bootstrap|jquery(?:-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(?:\.min)?\.js(?:\.map)?$`),
					regexp.MustCompile(`(?:^|/)javascript\.json$`),

					// ----------- Python files -----------
					// Dependencies and lock files.
					regexp.MustCompile(`(?:^|/)(?:Pipfile|poetry)\.lock$`),
					// Virtual environments
					regexp.MustCompile(`(?i)(?:^|/)(?:v?env|virtualenv)/lib(?:64)?(?:/.*)?$`),
					regexp.MustCompile(`(?i)(?:^|/)(?:lib(?:64)?/python[23](?:\.\d{1,2})+|python/[23](?:\.\d{1,2})+/lib(?:64)?)(?:/.*)?$`),
					// dist-info directory (https://py-pkgs.org/04-package-structure.html#building-sdists-and-wheels)
					regexp.MustCompile(`(?i)(?:^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(?:/.+)?$`),

					// ----------- Ruby files -----------
					regexp.MustCompile(`(?:^|/)vendor/(?:bundle|ruby)(?:/.*?)?$`),
					regexp.MustCompile(`\.gem$`), // tar archive

					// Misc
					regexp.MustCompile(`verification-metadata\.xml`),
					regexp.MustCompile(`Database.refactorlog`),

					// ----------- Git files ------------
					regexp.MustCompile(`(?:^|/)\.git$`),
				},
				StopWords: []string{
					"abcdefghijklmnopqrstuvwxyz", // character range
					// ----------- Secrets -----------
					// Checkmarx client secret. (https://github.com/checkmarx-ts/checkmarx-python-sdk/blob/86560f6e2a3e46d16322101294da10d5d190312d/README.md?plain=1#L56)
					"014df517-39d1-4453-b7b3-9930c563627c",
				},
			},
		},
	}
}

[evidence record sha256:b715d983b14df3a470032d3774693268b9a0bb128849e0f9cf1b215e88b9c7f2 kind tool-call:read]
step 9: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"head -n 2095 detect/detect_test.go | tail -n 45"}
tool shell ok: exit code: 0
stdout:
					EndColumn:   35,
					Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					File:        "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go",
					SymlinkFile: "",
					Tags:        []string{"key", "AWS"},
					Entropy:     3.0841837,
					Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
				},
			},
		},
		{
			source:           filepath.Join(archivesBasePath, "nested.tar.gz"),
			cfgName:          "archives",
			expireContext:    true,
			expectedFindings: []report.Finding{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			ctx, cancel := context.WithCancel(context.Background())
			if tt.expireContext {
				cancel()
			}

			cfg, _ := vc.Translate()
			detector := NewDetectorContext(ctx, cfg)
			detector.MaxArchiveDepth = 8

			findings, err := detector.DetectSource(
				ctx, &sources.Files{
					Path:            tt.source,
					Sema:            detector.Sema,
[evidence record sha256:9123ac5820ca51a1db25cb6c4356f06d09e86c9b81ae94800a7799a3bbe7f873 kind tool-call:shell]
step 10: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"awk 'NR >= 2075 && NR <= 2140' detect/detect_test.go"}
tool shell failed: denied: "awk 'NR >= 2075 && NR <= 2140' detect/detect_test.go" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:2a2661ba64ed7fc2dfbbad06acc34b470f85a2357d742a0ae7c5530058ddf924 kind tool-call:shell]
step 11: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect_test.go","maxBytes":8000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch (-want +got):\n%s", diff)
	}

}

func TestDetect(t *testing.T) {
	logging.Logger = logging.Logger.Level(zerolog.TraceLevel)
	tests := map[string]struct {
		cfgName      string
		baselinePath string
		fragment     Fragment
		// NOTE: for expected findings, all line numbers will be 0
		// because line deltas are added _after_ the finding is created.
		// I.e., if the finding is from a --no-git file, the line number will be
		// increase by 1 in DetectFromFiles(). If the finding is from git,
		// the line number will be increased by the patch delta.
		expectedFindings  []report.Finding
		wantError         error
		expectedAuxOutput string
	}{
		// General
		"valid allow comment (1)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OKIA\ // gitleaks:allow"`,
				FilePath: "tmp.go",
			},
		},
		"valid allow comment (2)": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \

		        \"AKIALALEMEL33243OKIA\ // gitleaks:allow"

		        `,
				FilePath: "tmp.go",
			},
		},
		"invalid allow comment": {
			cfgName: "simple",
			fragment: Fragment{
				Raw: `awsToken := \"AKIALALEMEL33243OKIA\"

		                // gitleaks:allow"

		                `,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					Description: "AWS Access Key",
					Secret:      "AKIALALEMEL33243OKIA",
					Match:       "AKIALALEMEL33243OKIA",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OKIA\"`,
					RuleID:      "aws-access-key",
					Tags:        []string{"key", "AWS"},
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Entropy:     3.1464393,
				},
			},
		},
		"detect finding - aws": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `awsToken := \"AKIALALEMEL33243OLIA\"`,
				FilePath: "tmp.go",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "aws-access-key",
					Description: "AWS Access Key",
					File:        "tmp.go",
					Line:        `awsToken := \"AKIALALEMEL33243OLIA\"`,
					Match:       "AKIALALEMEL33243OLIA",
					Secret:      "AKIALALEMEL33243OLIA",
					Entropy:     3.0841837,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 15,
					EndColumn:   34,
					Tags:        []string{"key", "AWS"},
				},
			},
		},
		"detect finding - sidekiq env var": {
			cfgName: "simple",
			fragment: Fragment{
				Raw:      `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
				FilePath: "tmp.sh",
			},
			expectedFindings: []report.Finding{
				{
					RuleID:      "sidekiq-secret",
					Description: "Sidekiq Secret",
					File:        "tmp.sh",
					Line:        `export BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;`,
					Match:       "BUNDLE_ENTERPRISE__CONTRIBSYS__COM=cafebabe:deadbeef;",
					Secret:      "cafebabe:deadbeef",
					Entropy:     2.6098502,
					StartLine:   0,
					EndLine:     0,
					StartColumn: 8,
					EndColumn:   60,
					Tags:    
[truncated at 8000 of 95196 bytes]
[evidence record sha256:b992c90f69c8417edff741a5bddd79234cb2a7dd71ca3809693750cf77ce426e kind tool-call:read]
tool search <- {"pattern":"context\\.WithCancel\\(ctx","path":"detect"}
tool search ok: no match for /context\.WithCancel\(ctx/
[evidence record sha256:45e547bd94c93022268919e8ef3158655f724263d9607ddb6347450ba686c5cb kind tool-call:search]
step 12: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect_test.go","maxBytes":5000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzIlNjUlNzQtdmFsdWU=
## one of the lines above that went through... a lot
## and there's surrounding text around it
Look at this value: %4EjMzMjU2NkE2MzZENTYzMDUwNTY3MDQ4%4eTY2RDcwNjk0RDY5NTUzMTRENkQ3ODYx%25%34%65TE3QTQ2MzY1NzZDNjQ0RjY1NTY3MDU5NTU1ODUyNkI2MjUzNTUzMDRFNkU0RTZCNTYzMTU1MzkwQQ== # isn't it crazy?
## Multi percent encode two random characters close to the bounds of the base64
## encoded data to make sure that the bounds are still correctly calculated
secret=ZG%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%36%25%33%31%25%32%35%25%33%32%25%33%35%25%32%35%25%33%33%25%33%36%25%32%35%25%33%33%25%33%322RlZC1zZWNyZXQtd%25%36%64%25%34%36%25%37%33dWU=
## The similar to the above but also touching the edge of the base64
secret=%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
## The similar to the above but also touching and overlapping the base64
secret%3D%25%35%61%25%34%37%25%35%36jb2RlZC1zZWNyZXQtdmFsdWU%25%32%35%25%33%33%25%36%34
`

var multili = `
username = "admin"



			password = "secret123"
`

func compare(t *testing.T, a, b []report.Finding) {
	if diff := cmp.Diff(a, b,
		cmpopts.SortSlices(func(a, b report.Finding) bool {
			if a.File != b.File {
				return a.File < b.File
			}
			if a.StartLine != b.StartLine {
				return a.StartLine < b.StartLine
			}
			if a.StartColumn != b.StartColumn {
				return a.StartColumn < b.StartColumn
			}
			if a.EndLine != b.EndLine {
				return a.EndLine < b.EndLine
			}
			if a.EndColumn != b.EndColumn {
				return a.EndColumn < b.EndColumn
			}
			if a.RuleID != b.RuleID {
				return a.RuleID < b.RuleID
			}
			return a.Secret < b.Secret
		}),
		cmpopts.IgnoreFields(report.Finding{},
			"Fingerprint", "Author", "Email", "Date", "Message", "Commit", "requiredFindings"),
		cmpopts.EquateApprox(0.0001, 0), // For floating point Entropy comparison
	); diff != "" {
		t.Errorf("findings mismatch 
[truncated at 5000 of 95196 bytes]
[evidence record sha256:6cd0259c05c20780c1f2668f232d86d51df932b7ba5bc83f5abdd0732fa217bd kind tool-call:read]
step 13: calling local:qwen3.6:35b-a3b
tool declare_file_set <- {"files":["cmd/generate/config/base/config.go","detect/detect_test.go"]}
tool declare_file_set ok: declared 2 file(s): cmd/generate/config/base/config.go, detect/detect_test.go
[evidence record sha256:a1e2afec629f816afbdb2575358cb97be5a04f082226474cab4600431ea2a458 kind tool-call:declare_file_set]
step 14: calling local:qwen3.6:35b-a3b
tool edit <- {"path":"cmd/generate/config/base/config.go","find":"\t\t\t\t\t\tfor _, char := range letters {\n\t\t\t\t\t\t\tif char != '*' || char == '.' {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"\\\\%c+\", char))\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"%c+\", char))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}","replace":"\t\t\t\t\t\tfor _, char := range letters {\n\t\t\t\t\t\t\tif char == '.' || char == '*' {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"\\\\%c+\", char))\n\t\t\t\t\t\t\t} else if char != 0 {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"%c+\", char))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}"}
tool edit ok: replaced 1 occurrence(s) in cmd/generate/config/base/config.go
[evidence record sha256:cad50c54f273ed7a511a27fcdab42d14f527a8173ca4a5f25276491aaf545f39 kind tool-call:edit]
step 15: calling local:qwen3.6:35b-a3b
tool edit <- {"path":"cmd/generate/config/base/config.go","find":"\t\t\t\t\t\tfor _, char := range letters {\n\t\t\t\t\t\t\tif char == '.' || char == '*' {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"\\\\%c+\", char))\n\t\t\t\t\t\t\t} else if char != 0 {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"%c+\", char))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}","replace":"\t\t\t\t\t\tfor _, char := range letters {\n\t\t\t\t\t\t\tif char == '.' || char == '*' {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"\\\\%c+\", char))\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"%c+\", char))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}"}
tool edit ok: replaced 1 occurrence(s) in cmd/generate/config/base/config.go
[evidence record sha256:fa637e10f7b5ff491a8e3096f792f3a52af3a5ad782efce6fc402dc22fff454c kind tool-call:edit]
step 16: calling local:qwen3.6:35b-a3b
tool edit <- {"path":"cmd/generate/config/base/config.go","find":"\t\t\t\t\t\tfor _, char := range letters {\n\t\t\t\t\t\t\tif char == '.' || char == '*' {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"\\\\%c+\", char))\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"%c+\", char))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}","replace":"\t\t\t\t\t\tfor _, char := range letters {\n\t\t\t\t\t\t\tif char == '.' || char == '*' {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"\\\\%c+\", char))\n\t\t\t\t\t\t\t} else if char >= 'a' && char <= 'z' {\n\t\t\t\t\t\t\t\tpatterns = append(patterns, fmt.Sprintf(\"%c+\", char))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}"}
tool edit ok: replaced 1 occurrence(s) in cmd/generate/config/base/config.go
[evidence record sha256:a3dbbbe0cb9919840a3b72ba9b4ab921cdf40102db1693fdeae62f4b3e43504e kind tool-call:edit]
step 17: calling local:qwen3.6:35b-a3b
tool read <- {"path":"cmd/generate/config/base/config.go","maxBytes":4000}
tool read ok: package base

import (
	"fmt"
	"strings"

	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/regexp"
)

func CreateGlobalConfig() config.Config {
	return config.Config{
		Title: "gitleaks config",
		Allowlists: []*config.Allowlist{
			{
				Description: "global allow lists",
				Regexes: []*regexp.Regexp{
					// ----------- General placeholders -----------
					regexp.MustCompile(`(?i)^true|false|null$`),
					// Awkward workaround to detect repeated characters.
					func() *regexp.Regexp {
						var (
							letters  = "abcdefghijklmnopqrstuvwxyz*."
							patterns []string
						)
						for _, char := range letters {
							if char == '.' || char == '*' {
								patterns = append(patterns, fmt.Sprintf("\\%c+", char))
							} else if char >= 'a' && char <= 'z' {
								patterns = append(patterns, fmt.Sprintf("%c+", char))
							}
						}
						return regexp.MustCompile("^(?i:" + strings.Join(patterns, "|") + ")$")
					}(),

					// ----------- Environment Variables -----------
					regexp.MustCompile(`^\$(?:\d+|{\d+})$`),
					regexp.MustCompile(`^\$(?:[A-Z_]+|[a-z_]+)$`),
					regexp.MustCompile(`^\${(?:[A-Z_]+|[a-z_]+)}$`),

					// ----------- Interpolated Variables -----------
					// Ansible (https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html)
					regexp.MustCompile(`^\{\{[ \t]*[\w ().|]+[ \t]*}}$`),
					// GitHub Actions
					// https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables
					// https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions
					regexp.MustCompile(`^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$`),
					// NuGet (https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables)
					regexp.MustCompile(`^%(?:[A-Z_]+|[a-z_]+)%$`),
					// String formatting.
					regexp.MustCompile(`^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$`), // Golang (https://pkg.go.dev/fmt)
					regexp.MustCompile(`^\{\d{0,2}}$`),                       // Python (https://docs.python.org/3/tutorial/inputoutput.html)
					// Urban Code Deploy (https://www.ibm.com/support/pages/replace-token-step-replaces-replacement-values-windows-variables)
					regexp.MustCompile(`^@(?:[A-Z_]+|[a-z_]+)@$`),

					// ----------- Miscellaneous -----------
					// File paths
					regexp.MustCompile(`^/Users/(?i)[a-z0-9]+/[\w .-/]+$`),              // MacOS
					regexp.MustCompile(`^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$`), // Linux
					// 11980 Jps -Dapplication.home=D:\develop_tools\jdk\jdk1.8.0_131 -Xms8m
					//regexp.MustCompile(`^$`), // Windows
				},
				Paths: []*regexp.Regexp{
					regexp.MustCompile(`gitleaks\.toml`),

					// ----------- Documents and media -----------
					regexp.MustCompile(`(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$`), // Images
					regexp.MustCompile(`(?i)\.(?:eot|[ot]tf|woff2?)$`),           // Fonts
					regexp.MustCompile(`(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$`),

					// ----------- Golang files -----------
					regexp.MustCompile(`go\.(?:mod|sum|work(?:\.sum)?)$`),
					regexp.MustCompile(`(?:^|/)vendor/modules\.txt$`),
					regexp.MustCompile(`(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$`),

					// ----------- Java files -----------
					// Gradle
					regexp.MustCompile(`(?:^|/)gradlew(?:\.bat)?$`),
					regexp.MustCompile(`(?:^|/)gradle\.lockfile$`),
					regexp.MustCompile(`(?:^|/)mvnw(?:\.cmd)?$`),
					regexp.MustCompile(`(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$`),

					// ----------- JavaScript files -----------
					// Dependencies and lock files.
					regexp.MustCompile(`(?:^|/)node_modules(?:/.*)?$`),
					regexp.MustCompile(`(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$`),
					regexp.MustCompile(`(
[truncated at 4000 of 5560 bytes]
[evidence record sha256:744dd16a1fe456d51e7fefb47d5aea6aed1214c234be0d92a4525a610d00f973 kind tool-call:read]
stopped: max-tokens after 17 steps, 1039021 tokens
gate typecheck passed: the command exited 0 [evidence record sha256:54817925db602c8e135097028cb7da4b31d3bd00dc99bb05a8ca7fcdb3257a79]
gate lint failed: the command exited 1 [evidence record sha256:a03c54f116bb9214fb2ab9365b24c9f5cadf03247ac837868d161d2feec77f47]
gate format failed: the command listed 152 offending file(s) [evidence record sha256:1140324ae120ad898e9e615d65a614422be1bbe28dddb58bf4c33acdf6f0829d]
gate tests passed: the command exited 0 [evidence record sha256:02d0e7f615b1604eb40f2268c19755f6bf0969448d834884678c7487f397d48c]
gate file-set passed: all 1 changed file(s) are inside the declared set of 2, and every one of them was declared before it was edited [evidence record sha256:54f6be5da5c3b4d8f1ac5d2a7902a6792d354999c684c1a718b0d9fce1451ec7]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:6ab4a60c77fbee0a6e8337589bb53775332ffb0cc3347a678c83b90c1c1257c8]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:0d1bddaaa0335ae01c8ded7b8c8df77a28e4d5968164123819f75b8963b3a344]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:e4544917a11298f6a754745d2738fe0f6523c4e67aad3f27582878f46f5e3ebd]
gate diff-budget passed (advisory): within budget: 1 file(s) and 2 added line(s) [evidence record sha256:e5d1b75086add4b841eb92a0fb0f91190a907f8de619bd97af325050a66a8017]
ratchet accepted attempt 1: the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage) [evidence record sha256:2f883d048c5cff16e4453b86568abeef2401dca96af13478e49d9e9269607664]
auto-resolve attempt 2 of 2
step 1: calling local:qwen3.6:35b-a3b
plan: I'll analyze the failing gates and fix them. Let me start by exploring the workspace structure and understanding the project.
tool list <- {"path":"."}
tool list ok: .campaign/
.git/
.github/
.gitignore
.gitleaks.toml
.gitleaksignore
.golangci.yaml
.goreleaser.yml
.pre-commit-hooks.yaml
CONTRIBUTING.md
Dockerfile
LICENSE
Makefile
README.md
SECURITY.md
USERS.md
cmd/
config/
detect/
go.mod
go.sum
logging/
main.go
regexp/
report/
report_templates/
scripts/
sources/
testdata/
version/
[evidence record sha256:6afbb28da3c4af64ab3ad33951d788d5d2afd9ae686e81c99f445b7876ee9544 kind tool-call:list]
tool shell <- {"command":"go list -m && head -20 go.mod 2>/dev/null || true"}
tool shell failed: denied: /dev/null resolves outside the workspace /work
[evidence record sha256:ac552f183a6c06ec11af4f2411db5ddcf9db3d5b5c6325ee059ad2471c5fa5e2 kind tool-call:shell]
step 2: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"cd /work && cat go.mod | head -5"}
tool shell failed: denied: "cd /work && cat go.mod | head -5" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:d6e561cfb09a011244c8caf1452e02d8e41cb4b43b638de62baa26f347256302 kind tool-call:shell]
tool read <- {"path":"detect/detect_test.go","maxBytes":3000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KEY-----

# Encoded
private_key: 'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCjQzNWYvYlJVQkhyYkhxTFkveFMzSTdPdGgrOHJnRyswdEJ3Zk1jYmswNVNneHE2UVV6U1lJUUFvcCtXdnNUd2syc1IrQzM4ZzBNbmIKdStRRGtnMHNwdz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K'

# Double Encoded: b64 encoded aws config inside a jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY29uZmlnIjoiVzJSbFptRjFiSFJkQ25KbFoybHZiaUE5SUhWekxXVmhjM1F0TWdwaGQzTmZZV05qWlhOelgydGxlVjlwWkNBOUlFRlRTVUZKVDFOR1QwUk9UamRNV0UweE1FcEpDbUYzYzE5elpXTnlaWFJmWVdOalpYTnpYMnRsZVNBOUlIZEtZV3h5V0ZWMGJrWkZUVWt2U3pkTlJFVk9SeTlpVUhoU1ptbERXVVZHVlVORWJFVllNVUVLIiwiaWF0IjoxNTE2MjM5MDIyfQ.8gxviXEOuIBQk2LvTYHSf-wXVhnEKC3h4yM5nlOF4zA

# A small secret at the end to make sure that as the other ones above shrink
# when decoded, the positions are taken into consideration for overlaps
c21hbGwtc2VjcmV0

# This tests how it handles when the match bounds go outside the decoded value
secret=ZGVjb2RlZC1zZWNyZXQtdmFsdWUwMA==
# The above encoded again
c2VjcmV0PVpHVmpiMlJsWkMxelpXTnlaWFF0ZG1Gc2RXVT0=

# Confirm you can ignore on the decoded value
password="bFJxQkstejVrZjQtcGxlYXNlLWlnbm9yZS1tZS1YLVhJSk0yUGRkdw=="

# This tests that it can do hex encoded data
secret=6465636F6465642D7365637265742D76616C756576484558

# This tests that it can do percent encoded data
## partial encoded data
secret=decoded-%73%65%63%72%65%74-valuev2
## scattered encoded
secret=%64%65coded-%73%65%63%72%65%74-valuev3

# Test multi levels of encoding where the source is a partal encoding
# it is important that the bounds of the predecessors are properly
# considered
## single percent encoding in the middle of multi layer b64
c2VjcmV0PVpHVmpiMl%4AsWkMxelpXTnlaWFF0ZG1Gc2RXVjJOQT09
## single percent encoding at the beginning of hex
secret%3d6465636F6465642D7365637265742D76616C75657635
## multiple percent encodings in a single layer base64
secret=ZGVjb2%52lZC1zZWNyZXQtdm%46sdWV4ODY=  # ends in x86
## base64 encoded partially percent encoded value
secret=ZGVjb2RlZC0lNzMlNjUlNjMlNzI
[truncated at 3000 of 95196 bytes]
[evidence record sha256:75e51f157715cff7a0b0594c5da3a1a2ae6bb1fe42060380ce7715c0b9cf99ac kind tool-call:read]
step 3: calling local:qwen3.6:35b-a3b
tool read <- {"path":"detect/detect_test.go","maxBytes":1000}
tool read ok: package detect

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/rs/zerolog"
	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/maps"

	"github.com/zricethezav/gitleaks/v8/cmd/scm"
	"github.com/zricethezav/gitleaks/v8/config"
	"github.com/zricethezav/gitleaks/v8/detect/codec"
	"github.com/zricethezav/gitleaks/v8/logging"
	"github.com/zricethezav/gitleaks/v8/regexp"
	"github.com/zricethezav/gitleaks/v8/report"
	"github.com/zricethezav/gitleaks/v8/sources"
)

const maxDecodeDepth = 8
const configPath = "../testdata/config/"
const repoBasePath = "../testdata/repos/"
const archivesBasePath = "../testdata/archives/"
const encodedTestValues = `
# Decoded
-----BEGIN PRIVATE KEY-----
135f/bRUBHrbHqLY/xS3I7Oth+8rgG+0tBwfMcbk05Sgxq6QUzSYIQAop+WvsTwk2sR+C38g0Mnb
u+QDkg0spw==
-----END PRIVATE KE
[truncated at 1000 of 95196 bytes]
[evidence record sha256:376398c8dd1408f9e8a25a0b596bb456ca5859e36db0ef5fd0cd152070d67e80 kind tool-call:read]
step 4: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"sed -n '2070,2140p' /work/detect/detect_test.go"}
tool shell ok: exit code: 0
stdout:

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			ctx, cancel := context.WithCancel(context.Background())
			if tt.expireContext {
				cancel()
			}

			cfg, _ := vc.Translate()
			detector := NewDetectorContext(ctx, cfg)
			detector.MaxArchiveDepth = 8

			findings, err := detector.DetectSource(
				ctx, &sources.Files{
					Path:            tt.source,
					Sema:            detector.Sema,
					Config:          &cfg,
					MaxArchiveDepth: detector.MaxArchiveDepth,
				},
			)

			if tt.expireContext {
				require.EqualError(t, err, "context canceled")
			} else {
				cancel()
				require.NoError(t, err)
			}

			// TODO: Temporary mitigation.
			// https://github.com/gitleaks/gitleaks/issues/1641
			normalizedFindings := make([]report.Finding, len(findings))
			for i, f := range findings {
				if strings.HasSuffix(f.Line, "\r") {
					f.Line = strings.ReplaceAll(f.Line, "\r", "")
				}
				if strings.HasSuffix(f.Match, "\r") {
					f.EndColumn = f.EndColumn - 1
					f.Match = strings.ReplaceAll(f.Match, "\r", "")
				}
				normalizedFindings[i] = f
			}
			assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
		})
	}

}

func TestDetectWithSymlinks(t *testing.T) {
	// TODO: Fix this test on windows.
	if runtime.GOOS == "windows" {
		t.Skipf("TODO: this returns no results on windows, I'm not sure why.")
		return
	}

	tests := []struct {
		cfgName          string
		source           string
		expectedFindings []report.Finding
	}{
		{
			source:  filepath.Join(repoBasePath, "symlinks/file_symlink"),
[evidence record sha256:add857a6db5d4ca6934c74f67ead5f1cb935baa209b3af9bdade9b413b055740 kind tool-call:shell]
step 5: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"cat -n /work/detect/detect_test.go | sed -n '2050,2130p'"}
tool shell ok: exit code: 0
stdout:
  2050						StartColumn: 16,
  2051						EndColumn:   35,
  2052						Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  2053						Match:       "AKIALALEMEL33243OLIA",
  2054						Secret:      "AKIALALEMEL33243OLIA",
  2055						File:        "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go",
  2056						SymlinkFile: "",
  2057						Tags:        []string{"key", "AWS"},
  2058						Entropy:     3.0841837,
  2059						Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
  2060					},
  2061				},
  2062			},
  2063			{
  2064				source:           filepath.Join(archivesBasePath, "nested.tar.gz"),
  2065				cfgName:          "archives",
  2066				expireContext:    true,
  2067				expectedFindings: []report.Finding{},
  2068			},
  2069		}
  2070	
  2071		for _, tt := range tests {
  2072			t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
  2073				viper.AddConfigPath(configPath)
  2074				viper.SetConfigName(tt.cfgName)
  2075				viper.SetConfigType("toml")
  2076				err := viper.ReadInConfig()
  2077				require.NoError(t, err)
  2078	
  2079				var vc config.ViperConfig
  2080				err = viper.Unmarshal(&vc)
  2081				require.NoError(t, err)
  2082	
  2083				ctx, cancel := context.WithCancel(context.Background())
  2084				if tt.expireContext {
  2085					cancel()
  2086				}
  2087	
  2088				cfg, _ := vc.Translate()
  2089				detector := NewDetectorContext(ctx, cfg)
  2090				detector.MaxArchiveDepth = 8
  2091	
  2092				findings, err := detector.DetectSource(
  2093					ctx, &sources.Files{
  2094						Path:            tt.source,
  2095						Sema:            detector.Sema,
  2096						Config:          &cfg,
  2097						MaxArchiveDepth: detector.MaxArchiveDepth,
  2098					},
  2099				)
  2100	
  2101				if tt.expireContext {
  2102					require.EqualError(t, err, "context canceled")
  2103				} else {
  2104					cancel()
  2105					require.NoError(t, err)
  2106				}
  2107	
  2108				// TODO: Temporary mitigation.
  2109				// https://github.com/gitleaks/gitleaks/issues/1641
  2110				normalizedFindings := make([]report.Finding, len(findings))
  2111				for i, f := range findings {
  2112					if strings.HasSuffix(f.Line, "\r") {
  2113						f.Line = strings.ReplaceAll(f.Line, "\r", "")
  2114					}
  2115					if strings.HasSuffix(f.Match, "\r") {
  2116						f.EndColumn = f.EndColumn - 1
  2117						f.Match = strings.ReplaceAll(f.Match, "\r", "")
  2118					}
  2119					normalizedFindings[i] = f
  2120				}
  2121				assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
  2122			})
  2123		}
  2124	
  2125	}
  2126	
  2127	func TestDetectWithSymlinks(t *testing.T) {
  2128		// TODO: Fix this test on windows.
  2129		if runtime.GOOS == "windows" {
  2130			t.Skipf("TODO: this returns no results on windows, I'm not sure why.")
[evidence record sha256:b56e1ee32c325449273f813a8228e4b0068dd863299335c2dc48f1830a521ca0 kind tool-call:shell]
step 6: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"cat -n /work/detect/detect_test.go | sed -n '2040,2130p'"}
tool shell ok: exit code: 0
stdout:
  2040						SymlinkFile: "",
  2041						Tags:        []string{"key", "AWS"},
  2042						Entropy:     3.0841837,
  2043						Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20",
  2044					},
  2045					{
  2046						RuleID:      "aws-access-key",
  2047						Description: "AWS Access Key",
  2048						StartLine:   20,
  2049						EndLine:     20,
  2050						StartColumn: 16,
  2051						EndColumn:   35,
  2052						Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  2053						Match:       "AKIALALEMEL33243OLIA",
  2054						Secret:      "AKIALALEMEL33243OLIA",
  2055						File:        "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go",
  2056						SymlinkFile: "",
  2057						Tags:        []string{"key", "AWS"},
  2058						Entropy:     3.0841837,
  2059						Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
  2060					},
  2061				},
  2062			},
  2063			{
  2064				source:           filepath.Join(archivesBasePath, "nested.tar.gz"),
  2065				cfgName:          "archives",
  2066				expireContext:    true,
  2067				expectedFindings: []report.Finding{},
  2068			},
  2069		}
  2070	
  2071		for _, tt := range tests {
  2072			t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
  2073				viper.AddConfigPath(configPath)
  2074				viper.SetConfigName(tt.cfgName)
  2075				viper.SetConfigType("toml")
  2076				err := viper.ReadInConfig()
  2077				require.NoError(t, err)
  2078	
  2079				var vc config.ViperConfig
  2080				err = viper.Unmarshal(&vc)
  2081				require.NoError(t, err)
  2082	
  2083				ctx, cancel := context.WithCancel(context.Background())
  2084				if tt.expireContext {
  2085					cancel()
  2086				}
  2087	
  2088				cfg, _ := vc.Translate()
  2089				detector := NewDetectorContext(ctx, cfg)
  2090				detector.MaxArchiveDepth = 8
  2091	
  2092				findings, err := detector.DetectSource(
  2093					ctx, &sources.Files{
  2094						Path:            tt.source,
  2095						Sema:            detector.Sema,
  2096						Config:          &cfg,
  2097						MaxArchiveDepth: detector.MaxArchiveDepth,
  2098					},
  2099				)
  2100	
  2101				if tt.expireContext {
  2102					require.EqualError(t, err, "context canceled")
  2103				} else {
  2104					cancel()
  2105					require.NoError(t, err)
  2106				}
  2107	
  2108				// TODO: Temporary mitigation.
  2109				// https://github.com/gitleaks/gitleaks/issues/1641
  2110				normalizedFindings := make([]report.Finding, len(findings))
  2111				for i, f := range findings {
  2112					if strings.HasSuffix(f.Line, "\r") {
  2113						f.Line = strings.ReplaceAll(f.Line, "\r", "")
  2114					}
  2115					if strings.HasSuffix(f.Match, "\r") {
  2116						f.EndColumn = f.EndColumn - 1
  2117						f.Match = strings.ReplaceAll(f.Match, "\r", "")
  2118					}
  2119					normalizedFindings[i] = f
  2120				}
  2121				assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
  2122			})
  2123		}
  2124	
  2125	}
  2126	
  2127	func TestDetectWithSymlinks(t *testing.T) {
  2128		// TODO: Fix this test on windows.
  2129		if runtime.GOOS == "windows" {
  2130			t.Skipf("TODO: this returns no results on windows, I'm not sure why.")
[evidence record sha256:2a912e623459c397ac1adaec0a3c6642e4f8e8727fbd1e692bc3b9735c975393 kind tool-call:shell]
step 7: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"grep -n 'func Test.*DetectArbitrary' /work/detect/detect_test.go | head -5"}
tool shell ok: exit code: 0
[evidence record sha256:ea8192b3fe1281964c2b0d11935930587468a1131952662dbbda7560b8480628 kind tool-call:shell]
tool shell <- {"command":"cat -n /work/detect/detect_test.go | sed -n '2010,2075p'"}
tool shell ok: exit code: 0
stdout:
  2010						Entropy:     3.0841837,
  2011						Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.zst:aws-access-key:20",
  2012					},
  2013					{
  2014						RuleID:      "aws-access-key",
  2015						Description: "AWS Access Key",
  2016						StartLine:   20,
  2017						EndLine:     20,
  2018						StartColumn: 16,
  2019						EndColumn:   35,
  2020						Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  2021						Match:       "AKIALALEMEL33243OLIA",
  2022						Secret:      "AKIALALEMEL33243OLIA",
  2023						File:        "../testdata/archives/nested.tar.gz!archives/files/main.go.gz",
  2024						SymlinkFile: "",
  2025						Tags:        []string{"key", "AWS"},
  2026						Entropy:     3.0841837,
  2027						Fingerprint: "../testdata/archives/nested.tar.gz!archives/files/main.go.gz:aws-access-key:20",
  2028					},
  2029					{
  2030						RuleID:      "aws-access-key",
  2031						Description: "AWS Access Key",
  2032						StartLine:   20,
  2033						EndLine:     20,
  2034						StartColumn: 16,
  2035						EndColumn:   35,
  2036						Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  2037						Match:       "AKIALALEMEL33243OLIA",
  2038						Secret:      "AKIALALEMEL33243OLIA",
  2039						File:        "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go",
  2040						SymlinkFile: "",
  2041						Tags:        []string{"key", "AWS"},
  2042						Entropy:     3.0841837,
  2043						Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/api.go:aws-access-key:20",
  2044					},
  2045					{
  2046						RuleID:      "aws-access-key",
  2047						Description: "AWS Access Key",
  2048						StartLine:   20,
  2049						EndLine:     20,
  2050						StartColumn: 16,
  2051						EndColumn:   35,
  2052						Line:        "\n\tawsToken := \"AKIALALEMEL33243OLIA\"",
  2053						Match:       "AKIALALEMEL33243OLIA",
  2054						Secret:      "AKIALALEMEL33243OLIA",
  2055						File:        "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go",
  2056						SymlinkFile: "",
  2057						Tags:        []string{"key", "AWS"},
  2058						Entropy:     3.0841837,
  2059						Fingerprint: "../testdata/archives/nested.tar.gz!archives/files.tar.xz!files/main.go:aws-access-key:20",
  2060					},
  2061				},
  2062			},
  2063			{
  2064				source:           filepath.Join(archivesBasePath, "nested.tar.gz"),
  2065				cfgName:          "archives",
  2066				expireContext:    true,
  2067				expectedFindings: []report.Finding{},
  2068			},
  2069		}
  2070	
  2071		for _, tt := range tests {
  2072			t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
  2073				viper.AddConfigPath(configPath)
  2074				viper.SetConfigName(tt.cfgName)
  2075				viper.SetConfigType("toml")
[evidence record sha256:891ba35cea876d1ed10d7790e1286cd755a5156959800370dde8ae12fee856fa kind tool-call:shell]
step 8: calling local:qwen3.6:35b-a3b
tool search <- {"pattern":"context\\.WithCancel","path":"/work/detect"}
tool search ok: detect/detect_test.go:2083: ctx, cancel := context.WithCancel(context.Background())
[evidence record sha256:18297bf69d7de286f6854efb8efed1a6df8d4f901ed6c67d7675764abfc37d60 kind tool-call:search]
step 9: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"head -1 /work/.gitignore 2>/dev/null; ls /work/.campaign/gomod/ 2>/dev/null | head -5"}
tool shell failed: denied: /dev/null resolves outside the workspace /work
[evidence record sha256:d4c755ff6b98060554243280c95dba17653dab78fbd7c624350c61ef6b8d6c93 kind tool-call:shell]
step 10: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go vet ./... 2>&1 | grep -v '\\.campaign/' | head -50"}
tool shell failed: denied: "go vet ./... 2>&1 | grep -v '\.campaign/' | head -50" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:5c37fafd46ef9a068d628153ca8221da6d66463d2b52edb0661077cb4bbf7a4d kind tool-call:shell]
tool shell <- {"command":"gofmt -l ./... 2>&1 | grep -v '\\.campaign/' | head -50"}
tool shell failed: denied: "gofmt -l ./... 2>&1 | grep -v '\.campaign/' | head -50" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:7c7bf402492b4f348eab94b573505f1f101212a22aa51033b583ba20f3d93b32 kind tool-call:shell]
step 11: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"go vet ./detect/ 2>&1"}
tool shell failed: denied: "go vet ./detect/ 2>&1" is not on the shell allowlist. Confirmation was declined.
[evidence record sha256:a6c87e2437c4ab5f4ee6a310662306c605a65c54e8e36670a9557f20f152bf90 kind tool-call:shell]
step 12: calling local:qwen3.6:35b-a3b
tool list <- {"path":".campaign"}
tool list ok: gocache/
gomod/
[evidence record sha256:8e4336824a8433124d7066d25cfa80095c58bcb854527a408563c60877857d11 kind tool-call:list]
step 13: calling local:qwen3.6:35b-a3b
tool search <- {"pattern":"func Test.*DetectArchi","path":"/work/detect"}
tool search ok: no match for /func Test.*DetectArchi/
[evidence record sha256:f46fc4331a7d9a3248523bd73a8ff14861eca1a558a254ed01913b63772fe60a kind tool-call:search]
step 14: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"ls -la /work/.campaign/gomod/github.com/ | head -10"}
tool shell ok: exit code: 0
stdout:
total 0
drwxr-xr-x  3 root root   96 Sep  4 06:08 !bobu!sumisu
drwxr-xr-x  5 root root  160 Sep  4 06:08 !masterminds
drwxr-xr-x  3 root root   96 Sep  4 06:08 !s!t!a!r!r!y-!s
drwxr-xr-x 45 root root 1440 Sep  4 06:08 .
drwxr-xr-x  9 root root  288 Sep  4 06:08 ..
drwxr-xr-x  3 root root   96 Sep  4 06:08 andybalholm
drwxr-xr-x  3 root root   96 Sep  4 06:08 aymanbagabas
drwxr-xr-x  5 root root  160 Sep  4 06:08 bodgit
drwxr-xr-x  3 root root   96 Sep  4 06:08 charmbracelet
[evidence record sha256:3d29e8e6a9e51e75e65d9f94d754240ceb3df44286d28c576908aa2886bed1ed kind tool-call:shell]
tool shell <- {"command":"cat /work/go.mod | grep -E 'gitleaks|replace'"}
tool shell ok: exit code: 0
stdout:
module github.com/zricethezav/gitleaks/v8
	github.com/gitleaks/go-gitdiff v0.9.1
[evidence record sha256:df95d7826388f34608324ea932ef475afafabae60efc8de8fb42cf57c2576615 kind tool-call:shell]
step 15: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"grep -n 'func Test' /work/detect/detect_test.go | grep 2060"}
tool shell ok: exit code: 1
[evidence record sha256:a8e1ebcd16b7dda7e47112f1bc767f0e1f1c47a6ad8c1a2206173154b8ca5bd0 kind tool-call:shell]
step 16: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"wc -l /work/detect/detect_test.go"}
tool shell ok: exit code: 0
stdout:
2773 /work/detect/detect_test.go
[evidence record sha256:2520519cbd941a21aed0e3510fe0ffc3f0b52664328a541226466347f8aa0c49 kind tool-call:shell]
step 17: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"awk '/^func Test/ {fn=$0} NR>=2060 && NR<=2130 {print NR\": \"$0}' /work/detect/detect_test.go | head -80"}
tool shell failed: denied: /^func Test/ {fn=$0} NR>=2060 && NR<=2130 {print NR": "$0} resolves outside the workspace /work
[evidence record sha256:80369a311b1c7da7cbc98bc3b11b9b752d5b34074573a9758da008e17b925a47 kind tool-call:shell]
step 18: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"sed -n '2060,2125p' /work/detect/detect_test.go"}
tool shell ok: exit code: 0
stdout:
				},
			},
		},
		{
			source:           filepath.Join(archivesBasePath, "nested.tar.gz"),
			cfgName:          "archives",
			expireContext:    true,
			expectedFindings: []report.Finding{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.cfgName+" - "+tt.source, func(t *testing.T) {
			viper.AddConfigPath(configPath)
			viper.SetConfigName(tt.cfgName)
			viper.SetConfigType("toml")
			err := viper.ReadInConfig()
			require.NoError(t, err)

			var vc config.ViperConfig
			err = viper.Unmarshal(&vc)
			require.NoError(t, err)

			ctx, cancel := context.WithCancel(context.Background())
			if tt.expireContext {
				cancel()
			}

			cfg, _ := vc.Translate()
			detector := NewDetectorContext(ctx, cfg)
			detector.MaxArchiveDepth = 8

			findings, err := detector.DetectSource(
				ctx, &sources.Files{
					Path:            tt.source,
					Sema:            detector.Sema,
					Config:          &cfg,
					MaxArchiveDepth: detector.MaxArchiveDepth,
				},
			)

			if tt.expireContext {
				require.EqualError(t, err, "context canceled")
			} else {
				cancel()
				require.NoError(t, err)
			}

			// TODO: Temporary mitigation.
			// https://github.com/gitleaks/gitleaks/issues/1641
			normalizedFindings := make([]report.Finding, len(findings))
			for i, f := range findings {
				if strings.HasSuffix(f.Line, "\r") {
					f.Line = strings.ReplaceAll(f.Line, "\r", "")
				}
				if strings.HasSuffix(f.Match, "\r") {
					f.EndColumn = f.EndColumn - 1
					f.Match = strings.ReplaceAll(f.Match, "\r", "")
				}
				normalizedFindings[i] = f
			}
			assert.ElementsMatch(t, tt.expectedFindings, normalizedFindings)
		})
	}

}
[evidence record sha256:523dd77f08ae71cb380f156c79820daa7cfc6565c7807ca1509c96cd1fa07732 kind tool-call:shell]
step 19: calling local:qwen3.6:35b-a3b
tool search <- {"pattern":"func Test.*Detect.*Archiv","path":"/work/detect"}
tool search ok: detect/detect_test.go:1529: func TestDetectWithArchives(t *testing.T) {
[evidence record sha256:338b310f0548406780e6fc9172c52585a9da1fe2af3331ea9bf9e9fe9ce0ae2d kind tool-call:search]
step 20: calling local:qwen3.6:35b-a3b
tool read <- {"path":".golangci.yaml"}
tool read ok: version: '2'
linters:
  default: none
  # It might be worth going through some of the disabled linters and enabling
  # them and fixing the items they call out e.g.: cyclop, prealloc,
  # paralleltest, prealloc, errcheck, dupl, unused, testifylint, gosec,
  # gocritic, perfsprint, exptostd, intrange, perfsprint (maybe others?)
  disable:
    - cyclop
    - depguard
    - dupl
    - err113
    - errcheck
    - exhaustive
    - exhaustruct
    - exptostd
    - forbidigo
    - funcorder
    - funlen
    - gochecknoglobals
    - gochecknoinits
    - gocognit
    - goconst
    - gocritic
    - gocyclo
    - godot
    - godox
    - gosec
    - gosmopolitan
    - intrange
    - lll
    - maintidx
    - mnd
    - musttag
    - nestif
    - nilerr
    - nlreturn
    - nonamedreturns
    - paralleltest
    - perfsprint
    - prealloc
    - predeclared
    - tagliatelle
    - testifylint
    - testpackage
    - tparallel
    - unparam
    - unused
    - varnamelen
    - wastedassign
    - whitespace
    - wrapcheck
    - wsl
    - zerologlint  # doesn't seem to catch gitleaks/v8/logging mistakes
  enable:
    - inamedparam
    - misspell
    - revive
    - misspell
    - inamedparam
    - exhaustruct
    - inamedparam
    - misspell
    - nonamedreturns
    - staticcheck
    - unconvert
  exclusions:
    rules:
      - linters:
          - staticcheck
        source: 'detector\.Detect\w+\(|sources\.DirectoryTargets\(|detect\.(?:Fragment|RemoteInfo)'
      - linters:
          - misspell
        source: '"(?:addres|busines|clas)",'
  settings:
    staticcheck:
      checks:
        - all
        - '-QF1001'
        - '-ST1000'
        - '-ST1003'
        - '-ST1018'
        - '-ST1020'
        - '-ST1021'
    revive:
      severity: error

[evidence record sha256:dbd98d411a453e3a5fece3b958d8a284bcd6e6465ab69c6dff4e49b0ceebdc83 kind tool-call:read]
step 21: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"find .campaign -name \"*.mod.bak\" 2>/dev/null; find .campaign -name \"*.sum.bak\" 2>/dev/null; ls /work/.campaign/"}
tool shell failed: denied: /dev/null resolves outside the workspace /work
[evidence record sha256:2280db5981b81f8b01a7614eaa9aa70dd11a0701a1a3fed6154da92f1adc1a2f kind tool-call:shell]
step 22: calling local:qwen3.6:35b-a3b
tool shell <- {"command":"head -5 go.mod"}
tool shell ok: exit code: 0
stdout:
module github.com/zricethezav/gitleaks/v8

go 1.24.11

require (
[evidence record sha256:b5036f046f5963c4214fb05f102df00241796d27793bcc3ff5d6d2afa669cb99 kind tool-call:shell]
tool shell <- {"command":"cat .gitignore 2>/dev/null | head -30"}
tool shell failed: denied: /dev/null resolves outside the workspace /work
[evidence record sha256:bf4b9d60957098f9bc87090949b6b8d98a4db167e669aeb24e6551a3849068a8 kind tool-call:shell]
step 23: calling local:qwen3.6:35b-a3b
claim (unverified): Let me analyze both failures in detail. The lint error is about a context being not used on all paths, and the format errors are in `.campaign/gomod/` which appears to be cached dependencies.
stopped: completed after 23 steps, 376902 tokens
gate typecheck passed: the command exited 0 [evidence record sha256:2966481ebf512b2b4c26bb07aec451fc966298927ce29d2b60e47f40ba1b8207]
gate lint failed: the command exited 1 [evidence record sha256:fb61843a7d384a0ede01d99eb1add51318d7e1fafc223e27b1842a1a05f79f38]
gate format failed: the command listed 152 offending file(s) [evidence record sha256:d5ec5741d91c5406ec3fc9d9b1ff9e36efdf12ebf64120409aa1abc3ab70897d]
gate tests passed: the command exited 0 [evidence record sha256:d499fc27971fc8560e945bc9b84bdcf1cdfc361bbfc944db826ba0104dda4e33]
gate file-set passed: all 1 changed file(s) are inside the declared set of 2, and every one of them was declared before it was edited [evidence record sha256:000619a0642dbca2cbcc8e1d63cc7d845b9d8ca05d0f0c8af5b663f20c2111de]
gate placeholder passed: no placeholder marker was introduced by this change [evidence record sha256:80a69ddf538b2edff3530ed1afc787650dc7d826a04eded035e165077cbe6257]
gate secret-scan passed: no known credential pattern appears in the added lines [evidence record sha256:48401cbfcc12987dfae6c002c84fc54a4e94fc5d6e443f68ed699bb2fc400bbf]
gate behaviour-probe passed: 0 changed function(s) still answer to their inputs. [evidence record sha256:d10ec5b4c8a1d40b28d094707e71408003a41b576e152d86d5eeb98612fa9caa]
gate diff-budget passed (advisory): within budget: 1 file(s) and 2 added line(s) [evidence record sha256:342e3a1d0fa1d19b1da2696c6d5d589eae9b243f48246e023f12620d2610d36d]
ratchet accepted attempt 2: the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage) [evidence record sha256:af20da92bbec6759f67e6452fc764f1483bc0deb2959a7b77d25d1f824f1bf8f]
escalated after 2 attempt(s) at gate lint: the command exited 1

gates:
  passed   typecheck: the command exited 0
  failed   lint: the command exited 1
  failed   format: the command listed 152 offending file(s)
  passed   tests: the command exited 0
  passed   file-set: all 1 changed file(s) are inside the declared set of 2, and every one of them was declared before it was edited
  passed   placeholder: no placeholder marker was introduced by this change
  passed   secret-scan: no known credential pattern appears in the added lines
  passed   behaviour-probe: 0 changed function(s) still answer to their inputs.
  passed   diff-budget (advisory): within budget: 1 file(s) and 2 added line(s)
attempt 1: accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
attempt 2: accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)

Escalating after 2 of 2 attempts.

Gate: lint (lint (go vet))
Why: the command exited 1
Its last run is ledger record sha256:fb61843a7d384a0ede01d99eb1add51318d7e1fafc223e27b1842a1a05f79f38.

Attempts:
  1. accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
     still failing: lint, format
  2. accepted - the ratchet accepted the attempt: no measure moved the wrong way (not compared: testsCollected, changedLineCoverage)
     still failing: lint, format

routing reward: 0.000 (the run escalated, so the gates never went green)
[signing] the Secret Service keyring would not take a new key (secret-tool store failed: ), so the bundle is signed with a per-run key

evidence bundle: /out/bundle
verify it anywhere: node /out/bundle/verify.mjs /out/bundle
review it: open /out/bundle/review.html
what this run produced

  the page a person reads: /out/bundle/review.html
  the bundle a stranger verifies: /out/bundle
  its own verifier, needing nothing installed: node /out/bundle/verify.mjs /out/bundle
  the chain every record is on: /out/bundle/ledger.jsonl

  277 records. The harness verified 1 claim(s) and refused 0.
  bundle verified in this run: verify.mjs exited 0
[chokepoint] refusing shell without a terminal to confirm on: "go test ./... 2>&1" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go test ./detect/... 2>&1; echo "EXIT:$?"" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "echo "hello"" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go version" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "which go 2>&1 || echo "not found"" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "awk 'NR >= 2075 && NR <= 2140' detect/detect_test.go" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "cd /work && cat go.mod | head -5" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go vet ./... 2>&1 | grep -v '\.campaign/' | head -50" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "gofmt -l ./... 2>&1 | grep -v '\.campaign/' | head -50" is not on the shell allowlist.
[chokepoint] refusing shell without a terminal to confirm on: "go vet ./detect/ 2>&1" is not on the shell allowlist.
