Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package trufflehog for openSUSE:Factory checked in at 2026-03-11 20:51:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/trufflehog (Old) and /work/SRC/openSUSE:Factory/.trufflehog.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "trufflehog" Wed Mar 11 20:51:25 2026 rev:116 rq:1338015 version:3.93.8 Changes: -------- --- /work/SRC/openSUSE:Factory/trufflehog/trufflehog.changes 2026-03-05 18:20:47.650202785 +0100 +++ /work/SRC/openSUSE:Factory/.trufflehog.new.8177/trufflehog.changes 2026-03-11 20:52:08.254090188 +0100 @@ -1,0 +2,7 @@ +Tue Mar 10 07:33:11 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 3.93.8: + * Stop growing filesystem resume data (#4797) + * fix: make LDAP verification context-aware (#4768) + +------------------------------------------------------------------- Old: ---- trufflehog-3.93.7.obscpio New: ---- trufflehog-3.93.8.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ trufflehog.spec ++++++ --- /var/tmp/diff_new_pack.FtgOK1/_old 2026-03-11 20:52:10.290172758 +0100 +++ /var/tmp/diff_new_pack.FtgOK1/_new 2026-03-11 20:52:10.290172758 +0100 @@ -17,7 +17,7 @@ Name: trufflehog -Version: 3.93.7 +Version: 3.93.8 Release: 0 Summary: CLI tool to find exposed secrets in source and archives License: AGPL-3.0-or-later ++++++ _service ++++++ --- /var/tmp/diff_new_pack.FtgOK1/_old 2026-03-11 20:52:10.326174218 +0100 +++ /var/tmp/diff_new_pack.FtgOK1/_new 2026-03-11 20:52:10.330174380 +0100 @@ -2,7 +2,7 @@ <service name="obs_scm" mode="manual"> <param name="url">https://github.com/trufflesecurity/trufflehog.git</param> <param name="scm">git</param> - <param name="revision">v3.93.7</param> + <param name="revision">v3.93.8</param> <param name="match-tag">v*</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.FtgOK1/_old 2026-03-11 20:52:10.350175191 +0100 +++ /var/tmp/diff_new_pack.FtgOK1/_new 2026-03-11 20:52:10.354175353 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/trufflesecurity/trufflehog.git</param> - <param name="changesrevision">c3e599b7163e8198a55467f3133db0e7b2a492cb</param></service></servicedata> + <param name="changesrevision">6c05c4a00b91aa542267d8e32a8254774799d68d</param></service></servicedata> (No newline at EOF) ++++++ trufflehog-3.93.7.obscpio -> trufflehog-3.93.8.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trufflehog-3.93.7/go.mod new/trufflehog-3.93.8/go.mod --- old/trufflehog-3.93.7/go.mod 2026-03-04 16:56:42.000000000 +0100 +++ new/trufflehog-3.93.8/go.mod 2026-03-09 21:02:57.000000000 +0100 @@ -46,7 +46,6 @@ github.com/getsentry/sentry-go v0.32.0 github.com/go-errors/errors v1.5.1 github.com/go-git/go-git/v5 v5.13.2 - github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 github.com/go-redis/redis v6.15.9+incompatible @@ -70,6 +69,7 @@ github.com/lestrrat-go/jwx/v3 v3.0.12 github.com/lib/pq v1.10.9 github.com/lrstanley/bubblezone v0.0.0-20250404061050-e13639e27357 + github.com/mariduv/ldap-verify v0.0.2 github.com/marusama/semaphore/v2 v2.5.0 github.com/mattn/go-isatty v0.0.20 github.com/mholt/archives v0.0.0-20241216060121-23e0af8fe73d @@ -130,7 +130,7 @@ dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/Azure/go-ntlmssp v0.1.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect github.com/DataDog/zstd v1.5.5 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trufflehog-3.93.7/go.sum new/trufflehog-3.93.8/go.sum --- old/trufflehog-3.93.7/go.sum 2026-03-04 16:56:42.000000000 +0100 +++ new/trufflehog-3.93.8/go.sum 2026-03-09 21:02:57.000000000 +0100 @@ -57,8 +57,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= +github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g= @@ -96,8 +96,6 @@ github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -327,8 +325,6 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= -github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -450,8 +446,6 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= -github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= -github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -471,18 +465,6 @@ github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= -github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= -github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= -github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg= @@ -546,6 +528,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mariduv/ldap-verify v0.0.2 h1:NBdDTYyWDr71CONVcizasqL/AA9tQ2RNgLhTgnyfquI= +github.com/mariduv/ldap-verify v0.0.2/go.mod h1:d/7+kkMBGDs9LPZ/7hmduYqtOkRIJcgpa8dL+9CsveE= github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM= github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trufflehog-3.93.7/pkg/detectors/ldap/ldap.go new/trufflehog-3.93.8/pkg/detectors/ldap/ldap.go --- old/trufflehog-3.93.7/pkg/detectors/ldap/ldap.go 2026-03-04 16:56:42.000000000 +0100 +++ new/trufflehog-3.93.8/pkg/detectors/ldap/ldap.go 2026-03-09 21:02:57.000000000 +0100 @@ -3,13 +3,14 @@ import ( "context" "crypto/tls" + "errors" "fmt" "net" "net/url" "strings" "time" - "github.com/go-ldap/ldap/v3" + ldap "github.com/mariduv/ldap-verify" regexp "github.com/wasilibs/go-re2" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" @@ -72,7 +73,7 @@ } if verify { - verificationErr := verifyLDAP(username[1], password[1], ldapURL) + verificationErr := verifyLDAP(ctx, username[1], password[1], ldapURL) s1.Verified = verificationErr == nil if !isErrDeterminate(verificationErr) { s1.SetVerificationError(verificationErr, password[1]) @@ -102,7 +103,7 @@ } if verify { - verificationError := verifyLDAP(username, password, ldapURL) + verificationError := verifyLDAP(ctx, username, password, ldapURL) s1.Verified = verificationError == nil if !isErrDeterminate(verificationError) { @@ -117,18 +118,18 @@ } func isErrDeterminate(err error) bool { - switch e := err.(type) { - case *ldap.Error: - switch e.Err.(type) { - case *net.OpError: - return false - } + var neterr *net.OpError + + if errors.As(err, &neterr) || + errors.Is(err, context.DeadlineExceeded) || + errors.Is(err, context.Canceled) { + return false } return true } -func verifyLDAP(username, password string, ldapURL *url.URL) error { +func verifyLDAP(ctx context.Context, username, password string, ldapURL *url.URL) error { // Tests with non-TLS, TLS, and STARTTLS uri := ldapURL.String() @@ -136,13 +137,13 @@ switch ldapURL.Scheme { case "ldap": // Non-TLS dial - l, err := ldap.DialURL(uri) + l, err := ldap.DialURL(uri, ldap.DialWithContext(ctx)) if err != nil { return err } defer l.Close() // Non-TLS verify - err = l.Bind(username, password) + err = l.BindContext(ctx, username, password) if err == nil { return nil } @@ -153,20 +154,23 @@ return err } // STARTTLS verify - return l.Bind(username, password) + return l.BindContext(ctx, username, password) case "ldaps": // TLS dial - l, err := ldap.DialURL(uri, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true})) + l, err := ldap.DialURL( + uri, + ldap.DialWithContext(ctx), + ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}), + ) if err != nil { return err } defer l.Close() // TLS verify - return l.Bind(username, password) + return l.BindContext(ctx, username, password) default: return fmt.Errorf("unknown ldap scheme %q", ldapURL.Scheme) } - } func (s Scanner) Type() detectorspb.DetectorType { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trufflehog-3.93.7/pkg/detectors/ldap/ldap_test.go new/trufflehog-3.93.8/pkg/detectors/ldap/ldap_test.go --- old/trufflehog-3.93.7/pkg/detectors/ldap/ldap_test.go 2026-03-04 16:56:42.000000000 +0100 +++ new/trufflehog-3.93.8/pkg/detectors/ldap/ldap_test.go 2026-03-09 21:02:57.000000000 +0100 @@ -3,9 +3,11 @@ import ( "context" "fmt" + "net" "testing" "github.com/google/go-cmp/cmp" + ldap "github.com/mariduv/ldap-verify" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" @@ -84,3 +86,25 @@ }) } } + +func Test_isErrDeterminate(t *testing.T) { + if isErrDeterminate(fmt.Errorf("anything")) != true { + t.Errorf("general errors should be determinate") + } + + if isErrDeterminate(&ldap.Error{Err: fmt.Errorf("anything")}) != true { + t.Errorf("ldap general errors should be determinate") + } + + if isErrDeterminate(&ldap.Error{Err: &net.OpError{}}) == true { + t.Errorf("ldap net.OpError{} should be indeterminate") + } + + if isErrDeterminate(&ldap.Error{Err: context.DeadlineExceeded}) == true { + t.Errorf("ldap context deadline should be indeterminate") + } + + if isErrDeterminate(&ldap.Error{Err: context.Canceled}) == true { + t.Errorf("ldap context deadline should be indeterminate") + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trufflehog-3.93.7/pkg/sources/filesystem/filesystem.go new/trufflehog-3.93.8/pkg/sources/filesystem/filesystem.go --- old/trufflehog-3.93.7/pkg/sources/filesystem/filesystem.go 2026-03-04 16:56:42.000000000 +0100 +++ new/trufflehog-3.93.8/pkg/sources/filesystem/filesystem.go 2026-03-09 21:02:57.000000000 +0100 @@ -5,6 +5,7 @@ "io" "os" "path/filepath" + "strings" "github.com/go-errors/errors" "github.com/go-logr/logr" @@ -136,7 +137,7 @@ initialDepth := 1 err = s.scanSymlink(ctx, cleanPath, chunksChan, workerPool, initialDepth, path) _ = workerPool.Wait() - s.ClearEncodedResumeContainingId(path + "#") + s.ClearEncodedResumeInfoFor(path) } else if fileInfo.IsDir() { ctx.Logger().V(5).Info("Root path is a dir", "path", cleanPath) workerPool := new(errgroup.Group) @@ -144,7 +145,7 @@ initialDepth := 1 err = s.scanDir(ctx, cleanPath, chunksChan, workerPool, initialDepth, path) _ = workerPool.Wait() - s.ClearEncodedResumeContainingId(path + "#") + s.ClearEncodedResumeInfoFor(path) } else { if !fileInfo.Mode().IsRegular() { logger.Info("skipping non-regular file", "path", cleanPath) @@ -217,13 +218,11 @@ if s.filter != nil && !s.filter.Pass(resolvedPath) { return nil } - resumptionKey := rootPath + "#" + path - startState := s.GetEncodedResumeInfoFor(resumptionKey) - resuming := startState != "" - if resuming && startState == resolvedPath { - ctx.Logger().V(5).Info("skipping symlink, already scanned", "path", resolvedPath) - return nil - } + + // Use a single resumption key for the entire scan rooted at rootPath. + // Resume checks are handled by the calling scanDir function. + resumptionKey := rootPath + workerPool.Go(func() error { if !fileInfo.Mode().Type().IsRegular() { ctx.Logger().V(5).Info("skipping non-regular file", "path", resolvedPath) @@ -232,7 +231,7 @@ if err := s.scanFile(ctx, resolvedPath, chunksChan); err != nil { ctx.Logger().Error(err, "error scanning file", "path", resolvedPath) } - s.SetEncodedResumeInfoFor(resumptionKey, resolvedPath) + s.SetEncodedResumeInfoFor(resumptionKey, path) return nil }) @@ -249,12 +248,35 @@ ) error { // check if the full path is not matching any pattern in include // FilterRuleSet and matching any exclude FilterRuleSet. - resumptionKey := rootPath + "#" + path if s.filter != nil && s.filter.ShouldExclude(path) { return nil } - startState := s.GetEncodedResumeInfoFor(resumptionKey) - resuming := startState != "" + + // Use a single resumption key for the entire scan rooted at rootPath. + // The value stored is the full path of the last successfully scanned file. + // This avoids accumulating separate entries for each subdirectory visited. + resumptionKey := rootPath + resumeAfter := s.GetEncodedResumeInfoFor(resumptionKey) + + // Only consider resumption if the resume point is within this directory's subtree. + // Since os.ReadDir returns entries sorted by filename: + // - If we're scanning /root/ccc and the resume point is /root/bbb/file.txt, + // we've already passed it (bbb < ccc) and should process ccc normally. + // - If we're scanning /root/aaa and the resume point is /root/bbb/file.txt, + // we haven't reached it yet (aaa < bbb), so aaa was already fully scanned + // and should be skipped entirely. + if resumeAfter != "" && !strings.HasPrefix(resumeAfter, path+string(filepath.Separator)) && resumeAfter != path { + // Resume point is not in this subtree. Compare paths to determine if we + // should skip this directory (already scanned) or process it (already passed). + if path < resumeAfter { + // This directory comes before the resume point lexicographically, + // meaning it was already fully scanned. Skip it entirely. + return nil + } + // This directory comes after the resume point, so we've already passed + // the resume point. Process this directory normally. + resumeAfter = "" + } ctx.Logger().V(5).Info("Full path found is", "fullPath", path) @@ -271,11 +293,35 @@ } } - if resuming { - if entryPath == startState { - resuming = false + // Skip entries until we pass the resume point. + // We don't clear the resume info when we find the resume point - instead we + // keep it set until a new file is scanned. This ensures we don't lose progress + // if the scan is interrupted between finding the resume point and scanning + // the next file. + if resumeAfter != "" { + // If this entry is the resume point, stop skipping. + if entryPath == resumeAfter { + resumeAfter = "" + continue // Skip the resume point itself since it was already processed. + } + // If the resume point is within this entry (a descendant), we need to + // traverse into it to find where to resume. + if entry.IsDir() && strings.HasPrefix(resumeAfter, entryPath+string(filepath.Separator)) { + // Recurse into this directory to find the resume point. + if err := s.scanDir(ctx, entryPath, chunksChan, workerPool, depth, rootPath); err != nil { + ctx.Logger().Error(err, "error scanning directory", "path", entryPath) + } + // After recursing, clear local resumeAfter. The child scanDir will have + // handled resumption within its subtree, and subsequent entries in this + // directory should be processed normally. + resumeAfter = "" + continue } - } else if entry.Type()&os.ModeSymlink != 0 { + // Skip this entry - it comes before the resume point in traversal order. + continue + } + + if entry.Type()&os.ModeSymlink != 0 { ctx.Logger().V(5).Info("Entry found is a symlink", "path", entryPath) if !s.canFollowSymlinks() { // If the file or directory is a symlink but the followSymlinks is disable ignore the path @@ -401,7 +447,7 @@ initialDepth := 1 scanErr = s.scanSymlink(ctx, cleanPath, ch, workerPool, initialDepth, path) _ = workerPool.Wait() - s.ClearEncodedResumeContainingId(path + "#") + s.ClearEncodedResumeInfoFor(path) } else if fileInfo.IsDir() { ctx.Logger().V(5).Info("Root path is a dir", "path", cleanPath) @@ -411,7 +457,7 @@ // TODO: Finer grain error tracking of individual chunks. scanErr = s.scanDir(ctx, cleanPath, ch, workerPool, initialDepth, path) _ = workerPool.Wait() - s.ClearEncodedResumeContainingId(path + "#") + s.ClearEncodedResumeInfoFor(path) } else { ctx.Logger().V(5).Info("Root path is a file", "path", cleanPath) // TODO: Finer grain error tracking of individual diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trufflehog-3.93.7/pkg/sources/filesystem/filesystem_test.go new/trufflehog-3.93.8/pkg/sources/filesystem/filesystem_test.go --- old/trufflehog-3.93.7/pkg/sources/filesystem/filesystem_test.go 2026-03-04 16:56:42.000000000 +0100 +++ new/trufflehog-3.93.8/pkg/sources/filesystem/filesystem_test.go 2026-03-09 21:02:57.000000000 +0100 @@ -1,9 +1,12 @@ package filesystem import ( + "encoding/json" + "fmt" "os" "path/filepath" "strings" + "sync" "testing" "time" @@ -394,7 +397,7 @@ // Create an IncludePathsFile with the absolute path of the file includeFilePath := filepath.Join(testDir, "include.txt") err = os.WriteFile(includeFilePath, []byte(strings.ReplaceAll(filePath, `\`, `\\`)+"\n"), 0644) - require.NoError(t, err) + require.NoError(t, err) conn, err := anypb.New(&sourcespb.Filesystem{ IncludePathsFile: includeFilePath, @@ -464,6 +467,285 @@ require.NotContains(t, processedFiles, binaryFile, "Binary file should be skipped") } +func TestResumptionInfoDoesNotGrowWithSubdirectories(t *testing.T) { + ctx := context.AddLogger(t.Context()) + + // Create a deeply nested directory structure with files at each level. + // Structure: root/dir0/dir1/dir2/.../dir9, each containing a file. + rootDir, err := os.MkdirTemp("", "trufflehog-resumption-test") + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll(rootDir) }) + + const numSubdirs = 10 + currentDir := rootDir + for i := 0; i < numSubdirs; i++ { + // Create a file in the current directory + filePath := filepath.Join(currentDir, fmt.Sprintf("file%d.txt", i)) + err := os.WriteFile(filePath, []byte(fmt.Sprintf("content %d", i)), 0644) + require.NoError(t, err) + + // Create the next subdirectory + subDir := filepath.Join(currentDir, fmt.Sprintf("subdir%d", i)) + err = os.Mkdir(subDir, 0755) + require.NoError(t, err) + currentDir = subDir + } + // Create a file in the deepest directory + err = os.WriteFile(filepath.Join(currentDir, "deepest.txt"), []byte("deepest"), 0644) + require.NoError(t, err) + + conn, err := anypb.New(&sourcespb.Filesystem{MaxSymlinkDepth: 0}) + require.NoError(t, err) + + // Initialize the source. + s := Source{} + err = s.Init(ctx, "test resumption growth", 0, 0, true, conn, 1) + require.NoError(t, err) + + // Track the maximum size of EncodedResumeInfo during the scan. + var maxResumeInfoSize int + var mu sync.Mutex + + // We need to periodically check the resume info size during scanning. + // Run ChunkUnit in a goroutine and poll the progress. + done := make(chan struct{}) + go func() { + defer close(done) + reporter := sourcestest.TestReporter{} + err := s.ChunkUnit(ctx, sources.CommonSourceUnit{ + ID: rootDir, + }, &reporter) + require.NoError(t, err) + }() + + // Poll the resume info size while scanning is in progress. + ticker := time.NewTicker(1 * time.Millisecond) + defer ticker.Stop() + +polling: + for { + select { + case <-done: + break polling + case <-ticker.C: + progress := s.GetProgress() + mu.Lock() + if len(progress.EncodedResumeInfo) > maxResumeInfoSize { + maxResumeInfoSize = len(progress.EncodedResumeInfo) + } + mu.Unlock() + } + } + + // After scan completes, check the final state. + finalProgress := s.GetProgress() + t.Logf("Final EncodedResumeInfo length: %d", len(finalProgress.EncodedResumeInfo)) + t.Logf("Max EncodedResumeInfo length during scan: %d", maxResumeInfoSize) + + // Parse the resume info to count entries if it's not empty. + if maxResumeInfoSize > 0 { + var resumeMap map[string]string + err := json.Unmarshal([]byte(finalProgress.EncodedResumeInfo), &resumeMap) + if err == nil { + t.Logf("Final resume info entries: %d", len(resumeMap)) + } + } + + // The key assertion: resumption info should NOT grow proportionally with + // the number of subdirectories. During the scan, it should only track the + // current position, not accumulate entries for every directory visited. + // + // With proper implementation, resume info should have at most a few entries + // (e.g., one per directory being actively scanned), not one entry per + // directory that has ever been visited. + // + // A reasonable upper bound for resume info size: each entry is roughly + // "rootPath#subPath": "filePath". With temp paths ~50 chars, one entry is + // ~150 bytes with JSON overhead. For 10 directories, accumulation would + // mean ~1500+ bytes. A non-accumulating implementation should stay well + // under that. + const maxAcceptableResumeInfoSize = 300 // bytes - allows for ~2 entries max + assert.LessOrEqual(t, maxResumeInfoSize, maxAcceptableResumeInfoSize, + "Resume info grew to %d bytes during scan, suggesting accumulation across %d subdirectories. "+ + "Resume info should not accumulate entries for each subdirectory visited.", + maxResumeInfoSize, numSubdirs) +} + +func TestResumptionSkipsAlreadyScannedFiles(t *testing.T) { + ctx := context.Background() + + // Create a directory with files that have predictable alphabetical order. + rootDir, err := os.MkdirTemp("", "trufflehog-resumption-test") + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll(rootDir) }) + + // Create files with predictable names for sorting. + files := []string{"aaa.txt", "bbb.txt", "ccc.txt", "ddd.txt"} + for _, name := range files { + filePath := filepath.Join(rootDir, name) + err := os.WriteFile(filePath, []byte("content of "+name), 0644) + require.NoError(t, err) + } + + conn, err := anypb.New(&sourcespb.Filesystem{}) + require.NoError(t, err) + + // Initialize the source. + s := Source{} + err = s.Init(ctx, "test resumption", 0, 0, true, conn, 1) + require.NoError(t, err) + + // Pre-set the resume point to simulate a previous interrupted scan. + // Setting it to bbb.txt means we should skip aaa.txt and bbb.txt, + // and only scan ccc.txt and ddd.txt. + resumePoint := filepath.Join(rootDir, "bbb.txt") + s.SetEncodedResumeInfoFor(rootDir, resumePoint) + + // Run the scan. + reporter := sourcestest.TestReporter{} + err = s.ChunkUnit(ctx, sources.CommonSourceUnit{ID: rootDir}, &reporter) + require.NoError(t, err) + + // Collect scanned file names. + scannedFiles := make(map[string]bool) + for _, chunk := range reporter.Chunks { + file := chunk.SourceMetadata.GetFilesystem().GetFile() + scannedFiles[filepath.Base(file)] = true + } + + // Assert only files after the resume point were scanned. + assert.False(t, scannedFiles["aaa.txt"], "aaa.txt should have been skipped (before resume point)") + assert.False(t, scannedFiles["bbb.txt"], "bbb.txt should have been skipped (the resume point itself)") + assert.True(t, scannedFiles["ccc.txt"], "ccc.txt should have been scanned (after resume point)") + assert.True(t, scannedFiles["ddd.txt"], "ddd.txt should have been scanned (after resume point)") + assert.Equal(t, 2, len(reporter.Chunks), "expected exactly 2 files to be scanned") +} + +func TestResumptionWithNestedDirectories(t *testing.T) { + ctx := context.Background() + + // Create a nested directory structure: + // root/ + // aaa/ + // file1.txt + // bbb/ + // file2.txt + // ccc/ + // file3.txt + rootDir, err := os.MkdirTemp("", "trufflehog-resumption-nested-test") + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll(rootDir) }) + + dirs := []string{"aaa", "bbb", "ccc"} + for i, dir := range dirs { + dirPath := filepath.Join(rootDir, dir) + err := os.Mkdir(dirPath, 0755) + require.NoError(t, err) + + filePath := filepath.Join(dirPath, fmt.Sprintf("file%d.txt", i+1)) + err = os.WriteFile(filePath, []byte(fmt.Sprintf("content of file%d", i+1)), 0644) + require.NoError(t, err) + } + + conn, err := anypb.New(&sourcespb.Filesystem{}) + require.NoError(t, err) + + // Initialize the source. + s := Source{} + err = s.Init(ctx, "test resumption nested", 0, 0, true, conn, 1) + require.NoError(t, err) + + // Pre-set the resume point to bbb/file2.txt. + // This should skip aaa/file1.txt and bbb/file2.txt, only scanning ccc/file3.txt. + resumePoint := filepath.Join(rootDir, "bbb", "file2.txt") + s.SetEncodedResumeInfoFor(rootDir, resumePoint) + + // Run the scan. + reporter := sourcestest.TestReporter{} + err = s.ChunkUnit(ctx, sources.CommonSourceUnit{ID: rootDir}, &reporter) + require.NoError(t, err) + + // Collect scanned file names. + scannedFiles := make(map[string]bool) + for _, chunk := range reporter.Chunks { + file := chunk.SourceMetadata.GetFilesystem().GetFile() + scannedFiles[filepath.Base(file)] = true + } + + // Assert only file3.txt was scanned. + assert.False(t, scannedFiles["file1.txt"], "file1.txt should have been skipped (in aaa/, before resume point)") + assert.False(t, scannedFiles["file2.txt"], "file2.txt should have been skipped (the resume point itself)") + assert.True(t, scannedFiles["file3.txt"], "file3.txt should have been scanned (in ccc/, after resume point)") + assert.Equal(t, 1, len(reporter.Chunks), "expected exactly 1 file to be scanned") +} + +func TestResumptionWithOutOfSubtreeResumePoint(t *testing.T) { + ctx := context.Background() + + // Create a directory structure: + // root/ + // aaa/ + // file1.txt + // bbb/ + // file2.txt + // ccc/ + // file3.txt + // + // This test verifies correct behavior when scanDir is called for a directory + // with a resume point OUTSIDE that directory's subtree. Since os.ReadDir + // returns entries sorted by filename, directories that lexicographically + // precede the resume point were already fully scanned and should be skipped. + rootDir, err := os.MkdirTemp("", "trufflehog-resumption-subtree-test") + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll(rootDir) }) + + dirs := []string{"aaa", "bbb", "ccc"} + for i, dir := range dirs { + dirPath := filepath.Join(rootDir, dir) + err := os.Mkdir(dirPath, 0755) + require.NoError(t, err) + + filePath := filepath.Join(dirPath, fmt.Sprintf("file%d.txt", i+1)) + err = os.WriteFile(filePath, []byte(fmt.Sprintf("content of file%d", i+1)), 0644) + require.NoError(t, err) + } + + conn, err := anypb.New(&sourcespb.Filesystem{}) + require.NoError(t, err) + + // Initialize the source. + s := Source{} + err = s.Init(ctx, "test resumption subtree", 0, 0, true, conn, 1) + require.NoError(t, err) + + // Pre-set the resume point to bbb/file2.txt using aaaDir as the key. + // This simulates an edge case where scanDir is called directly for a + // directory with a resume point outside its subtree. + aaaDir := filepath.Join(rootDir, "aaa") + resumePoint := filepath.Join(rootDir, "bbb", "file2.txt") + s.SetEncodedResumeInfoFor(aaaDir, resumePoint) + + // Scan the aaa directory with a resume point outside its subtree. + reporter := sourcestest.TestReporter{} + err = s.ChunkUnit(ctx, sources.CommonSourceUnit{ID: aaaDir}, &reporter) + require.NoError(t, err) + + // Collect scanned file names. + scannedFiles := make(map[string]bool) + for _, chunk := range reporter.Chunks { + file := chunk.SourceMetadata.GetFilesystem().GetFile() + scannedFiles[filepath.Base(file)] = true + } + + // file1.txt should NOT be scanned because aaa/ comes before bbb/ + // lexicographically, meaning aaa/ would have been fully processed + // before reaching the resume point. + assert.False(t, scannedFiles["file1.txt"], + "file1.txt should NOT be scanned because aaa/ comes before resume point bbb/file2.txt lexicographically") + assert.Equal(t, 0, len(reporter.Chunks), + "expected 0 files to be scanned since aaa/ was already fully processed before the resume point") +} + // createTempFile is a helper function to create a temporary file in the given // directory with the provided contents. If dir is "", the operating system's // temp directory is used. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/trufflehog-3.93.7/pkg/sources/sources.go new/trufflehog-3.93.8/pkg/sources/sources.go --- old/trufflehog-3.93.7/pkg/sources/sources.go 2026-03-04 16:56:42.000000000 +0100 +++ new/trufflehog-3.93.8/pkg/sources/sources.go 2026-03-09 21:02:57.000000000 +0100 @@ -4,7 +4,6 @@ "encoding/json" "errors" "runtime" - "strings" "sync" "google.golang.org/protobuf/types/known/anypb" @@ -595,22 +594,6 @@ p.EncodedResumeInfo = marshalEncodedResumeInfo(p.encodedResumeInfoByID) } -// ClearEncodedResumeContainingId removes the encoded resume information -// entries that contain the id -func (p *Progress) ClearEncodedResumeContainingId(id string) { - p.mut.Lock() - defer p.mut.Unlock() - p.ensureEncodedResumeInfoByID() - - for key := range p.encodedResumeInfoByID { - if strings.Contains(key, id) { - delete(p.encodedResumeInfoByID, key) - } - } - - p.EncodedResumeInfo = marshalEncodedResumeInfo(p.encodedResumeInfoByID) -} - // ensureEncodedResumeInfoByID ensures the encodedResumeInfoByID attribute is a // non-nil map. The mutex must be held when calling this function. func (p *Progress) ensureEncodedResumeInfoByID() { ++++++ trufflehog.obsinfo ++++++ --- /var/tmp/diff_new_pack.FtgOK1/_old 2026-03-11 20:52:13.574305940 +0100 +++ /var/tmp/diff_new_pack.FtgOK1/_new 2026-03-11 20:52:13.578306102 +0100 @@ -1,5 +1,5 @@ name: trufflehog -version: 3.93.7 -mtime: 1772639802 -commit: c3e599b7163e8198a55467f3133db0e7b2a492cb +version: 3.93.8 +mtime: 1773086577 +commit: 6c05c4a00b91aa542267d8e32a8254774799d68d ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/trufflehog/vendor.tar.gz /work/SRC/openSUSE:Factory/.trufflehog.new.8177/vendor.tar.gz differ: char 13, line 1
