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-07-03 16:09:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/trufflehog (Old)
 and      /work/SRC/openSUSE:Factory/.trufflehog.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "trufflehog"

Fri Jul  3 16:09:36 2026 rev:126 rq:1363602 version:3.95.8

Changes:
--------
--- /work/SRC/openSUSE:Factory/trufflehog/trufflehog.changes    2026-07-02 
20:12:58.616710779 +0200
+++ /work/SRC/openSUSE:Factory/.trufflehog.new.1982/trufflehog.changes  
2026-07-03 16:09:40.619187446 +0200
@@ -1,0 +2,12 @@
+Fri Jul 03 04:58:04 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 3.95.8:
+  * Include encoded resume info instead of clobbering it (#5110)
+  * fixed syntax error (#5109)
+  * [INS-334] Octopus Deploy detector (#4787)
+  * [INS-465] Skip unverified JWT Detector results when feature flag is 
enabled (#5072)
+  * Add prometheus metrics for engine channels and workers (#5095)
+  * fix(azuresastoken): match SAS tokens regardless of parameter order (#5043)
+  * removed "unauthorized" as exception for rotated graphana secrets (#5068)
+
+-------------------------------------------------------------------

Old:
----
  trufflehog-3.95.7.obscpio

New:
----
  trufflehog-3.95.8.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ trufflehog.spec ++++++
--- /var/tmp/diff_new_pack.m322Bh/_old  2026-07-03 16:09:44.363317299 +0200
+++ /var/tmp/diff_new_pack.m322Bh/_new  2026-07-03 16:09:44.367317437 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           trufflehog
-Version:        3.95.7
+Version:        3.95.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.m322Bh/_old  2026-07-03 16:09:44.443320073 +0200
+++ /var/tmp/diff_new_pack.m322Bh/_new  2026-07-03 16:09:44.451320351 +0200
@@ -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">refs/tags/v3.95.7</param>
+    <param name="revision">refs/tags/v3.95.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.m322Bh/_old  2026-07-03 16:09:44.479321322 +0200
+++ /var/tmp/diff_new_pack.m322Bh/_new  2026-07-03 16:09:44.483321460 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/trufflesecurity/trufflehog.git</param>
-              <param 
name="changesrevision">f446421baf832d6356c42c1743d99abff52ff334</param></service></servicedata>
+              <param 
name="changesrevision">00155c9dc586f34d189adc83d3ac2698c2ec551f</param></service></servicedata>
 (No newline at EOF)
 

++++++ trufflehog-3.95.7.obscpio -> trufflehog-3.95.8.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/docs/man/trufflehog.1 
new/trufflehog-3.95.8/docs/man/trufflehog.1
--- old/trufflehog-3.95.7/docs/man/trufflehog.1 2026-06-29 12:17:14.000000000 
+0200
+++ new/trufflehog-3.95.8/docs/man/trufflehog.1 2026-07-02 20:52:07.000000000 
+0200
@@ -116,6 +116,9 @@
 \fB--user-agent-suffix=USER-AGENT-SUFFIX\fR
 Suffix to add to User-Agent.
 .TP
+\fB--drop-unverified-jwt-results\fR
+Drop unverified results without any verification errors from the JWT detector.
+.TP
 \fB--version\fR
 Show application version.
 .SH COMMANDS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/main.go 
new/trufflehog-3.95.8/main.go
--- old/trufflehog-3.95.7/main.go       2026-06-29 12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/main.go       2026-07-02 20:52:07.000000000 +0200
@@ -87,11 +87,12 @@
        noVerificationCache = cli.Flag("no-verification-cache", "Disable 
verification caching").Bool()
 
        // Add feature flags
-       forceSkipBinaries  = cli.Flag("force-skip-binaries", "Force skipping 
binaries.").Bool()
-       forceSkipArchives  = cli.Flag("force-skip-archives", "Force skipping 
archives.").Bool()
-       gitCloneTimeout    = cli.Flag("git-clone-timeout", "Maximum time to 
spend cloning a repository, as a duration.").Hidden().Duration()
-       skipAdditionalRefs = cli.Flag("skip-additional-refs", "Skip additional 
references.").Bool()
-       userAgentSuffix    = cli.Flag("user-agent-suffix", "Suffix to add to 
User-Agent.").String()
+       forceSkipBinaries        = cli.Flag("force-skip-binaries", "Force 
skipping binaries.").Bool()
+       forceSkipArchives        = cli.Flag("force-skip-archives", "Force 
skipping archives.").Bool()
+       gitCloneTimeout          = cli.Flag("git-clone-timeout", "Maximum time 
to spend cloning a repository, as a duration.").Hidden().Duration()
+       skipAdditionalRefs       = cli.Flag("skip-additional-refs", "Skip 
additional references.").Bool()
+       userAgentSuffix          = cli.Flag("user-agent-suffix", "Suffix to add 
to User-Agent.").String()
+       dropUnverifiedJWTResults = cli.Flag("drop-unverified-jwt-results", 
"Drop unverified results without any verification errors from the JWT 
detector.").Bool()
 
        gitScan                = cli.Command("git", "Find credentials in git 
repositories.")
        gitScanURI             = gitScan.Arg("uri", "Git repository URL. 
https://, file://, or ssh:// schema expected.").Required().String()
@@ -516,6 +517,10 @@
                feature.UserAgentSuffix.Store(*userAgentSuffix)
        }
 
+       if *dropUnverifiedJWTResults {
+               feature.DropUnverifiedJWTResults.Store(true)
+       }
+
        // OSS Default APK handling on
        feature.EnableAPKHandler.Store(true)
 
@@ -545,6 +550,7 @@
        feature.BraintrustDetectorEnabled.Store(true)
        feature.PgAnalyzeReadKeyDetectorEnabled.Store(true)
        feature.RedHatPyxisDetectorEnabled.Store(true)
+       feature.OctopusDeployDetectorEnabled.Store(true)
 
        conf := &config.Config{}
        if *configFilename != "" {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/detectors/azuresastoken/azuresastoken.go 
new/trufflehog-3.95.8/pkg/detectors/azuresastoken/azuresastoken.go
--- old/trufflehog-3.95.7/pkg/detectors/azuresastoken/azuresastoken.go  
2026-06-29 12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/detectors/azuresastoken/azuresastoken.go  
2026-07-02 20:52:07.000000000 +0200
@@ -31,9 +31,29 @@
        // microsoft storage resource naming rules: 
https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftstorage:~:text=format%3A%0AVaultName_KeyName_KeyVersion.-,Microsoft.Storage,-Expand%20table
        urlPat = 
regexp.MustCompile(`https://([a-zA-Z0-9][a-z0-9_-]{1,22}[a-zA-Z0-9])\.blob\.core\.windows\.net/[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?(?:/[a-zA-Z0-9._-]+)*`)
 
-       keyPat = regexp.MustCompile(
-               detectors.PrefixRegex([]string{"azure", "sas", "token", "blob", 
".blob.core.windows.net"}) +
-                       
`(sp=[racwdli]+&st=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z&se=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z(?:&sip=\d{1,3}(?:\.\d{1,3}){3}(?:-\d{1,3}(?:\.\d{1,3}){3})?)?(&spr=https)?(?:,https)?&sv=\d{4}-\d{2}-\d{2}&sr=[bcfso]&sig=[a-zA-Z0-9%]{10,})`)
+       // A SAS token is a query string of `&`-joined `key=value` pairs. The 
parameters
+       // can appear in any order, and values may be URL-encoded (e.g. Azure 
Storage
+       // Explorer encodes the `:` in the timestamps as %3A and the `+`, `/`, 
`=` in the
+       // signature as %2B, %2F, %3D). Rather than enumerate every permutation 
in one
+       // regex, match a contiguous run of query parameters and validate the 
SAS-specific
+       // parameters in keyMatchIsSASToken below. This keeps detection 
order-independent.
+       // The value class excludes `=` so a SAS token preceded by a short 
lowercase
+       // `key=` (e.g. `sas=sp=r&...`) is not absorbed from that preceding 
key, which
+       // would bury the real `sp` parameter inside the first value and fail
+       // validation. Azure URL-encodes any `=` inside a value (the signature 
padding
+       // becomes %3D), so excluding the literal does not drop a legitimate 
value.
+       sasQueryPat = 
regexp.MustCompile(`[a-z]{2,4}=[^&=\s"'<>]+(?:&[a-z]{2,4}=[^&=\s"'<>]+)+`)
+
+       // Validators for the individual SAS parameters. The `sp` permission 
set and `sr`
+       // resource set match the formats the previous, order-locked regex 
accepted.
+       spValuePat = regexp.MustCompile(`^[racwdli]+$`)
+       svValuePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
+       srValuePat = regexp.MustCompile(`^[bcfso]$`)
+       // The `:` separators may be URL-encoded; percent-encoding hex digits 
are
+       // case-insensitive per RFC 3986, so accept both %3A and %3a.
+       timeValuePat = 
regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}(?::|%3[Aa])\d{2}(?::|%3[Aa])\d{2}Z$`)
+       sigValuePat  = regexp.MustCompile(`^[a-zA-Z0-9%+/=]{10,}$`)
+       sipValuePat  = 
regexp.MustCompile(`^\d{1,3}(?:\.\d{1,3}){3}(?:-\d{1,3}(?:\.\d{1,3}){3})?$`)
 
        invalidStorageAccounts = simple.NewCache[struct{}]()
 
@@ -55,6 +75,60 @@
        return "An Azure Shared Access Signature (SAS) token is a time-limited, 
permission-based URL query string that grants secure, granular access to Azure 
Storage resources (e.g., blobs, containers, files) without exposing account 
keys."
 }
 
+// keyMatchIsSASToken reports whether a matched query-parameter run is a Azure
+// Storage SAS token. The required parameters may appear in any order; values 
may
+// be URL-encoded. This reproduces the validations the previous order-locked 
regex
+// enforced (permission set, resource type, timestamp shape, signature, 
optional IP)
+// without depending on parameter order.
+func keyMatchIsSASToken(query string) bool {
+       params := make(map[string]string)
+       for _, pair := range strings.Split(query, "&") {
+               key, value, ok := strings.Cut(pair, "=")
+               if !ok {
+                       continue
+               }
+               params[key] = value
+       }
+
+       sp, ok := params["sp"]
+       if !ok || !spValuePat.MatchString(sp) {
+               return false
+       }
+       sv, ok := params["sv"]
+       if !ok || !svValuePat.MatchString(sv) {
+               return false
+       }
+       sr, ok := params["sr"]
+       if !ok || !srValuePat.MatchString(sr) {
+               return false
+       }
+       sig, ok := params["sig"]
+       if !ok || !sigValuePat.MatchString(sig) {
+               return false
+       }
+
+       // A SAS token carries a start time, an expiry time, or both; any 
present one
+       // must be well-formed.
+       st, hasStart := params["st"]
+       se, hasExpiry := params["se"]
+       if !hasStart && !hasExpiry {
+               return false
+       }
+       if hasStart && !timeValuePat.MatchString(st) {
+               return false
+       }
+       if hasExpiry && !timeValuePat.MatchString(se) {
+               return false
+       }
+
+       // The IP restriction is optional, but must be a valid address or range 
if set.
+       if sip, ok := params["sip"]; ok && !sipValuePat.MatchString(sip) {
+               return false
+       }
+
+       return true
+}
+
 func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) 
(results []detectors.Result, err error) {
        logger := logContext.AddLogger(ctx).Logger().WithName("azuresas")
 
@@ -68,8 +142,10 @@
 
        // deduplicate keyMatches
        keyMatchesUnique := make(map[string]struct{})
-       for _, keyMatch := range keyPat.FindAllStringSubmatch(dataStr, -1) {
-               keyMatchesUnique[keyMatch[1]] = struct{}{}
+       for _, keyMatch := range sasQueryPat.FindAllString(dataStr, -1) {
+               if keyMatchIsSASToken(keyMatch) {
+                       keyMatchesUnique[keyMatch] = struct{}{}
+               }
        }
 
        // Check results.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/detectors/azuresastoken/azuresastoken_test.go 
new/trufflehog-3.95.8/pkg/detectors/azuresastoken/azuresastoken_test.go
--- old/trufflehog-3.95.7/pkg/detectors/azuresastoken/azuresastoken_test.go     
2026-06-29 12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/detectors/azuresastoken/azuresastoken_test.go     
2026-07-02 20:52:07.000000000 +0200
@@ -61,6 +61,43 @@
                        want: 
[]string{"https://trufflesecurity.blob.core.windows.net/trufflesecurity/test_blob.txtsp=r&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D"},
                },
                {
+                       // Storage Explorer emits the parameters in a different 
order (sv first,
+                       // sp last) and URL-encodes the ':' in the timestamps 
as %3A. See #4732.
+                       name: "valid pattern, alternate order with url-encoded 
timestamps",
+                       input: `
+       
https://accountname.blob.core.windows.net/sorted?sv=2023-01-03&st=2025-06-18T08%3A45%3A11Z&se=2025-06-19T08%3A45%3A11Z&sr=c&sp=r&sig=ow2a1XbXmD4%2BEv9LBUkek8R%2FrAjvrQFpenUbzztILn8%3D
+       `,
+                       want: 
[]string{"https://accountname.blob.core.windows.net/sortedsv=2023-01-03&st=2025-06-18T08%3A45%3A11Z&se=2025-06-19T08%3A45%3A11Z&sr=c&sp=r&sig=ow2a1XbXmD4%2BEv9LBUkek8R%2FrAjvrQFpenUbzztILn8%3D"},
+               },
+               {
+                       // Percent-encoding hex digits are case-insensitive 
(RFC 3986), so a
+                       // timestamp encoded with lowercase %3a must match just 
like %3A.
+                       name: "valid pattern, url-encoded timestamps with 
lowercase hex",
+                       input: `
+       
https://accountname.blob.core.windows.net/sorted?sv=2023-01-03&st=2025-06-18T08%3a45%3a11Z&se=2025-06-19T08%3a45%3a11Z&sr=c&sp=r&sig=ow2a1XbXmD4%2BEv9LBUkek8R%2FrAjvrQFpenUbzztILn8%3D
+       `,
+                       want: 
[]string{"https://accountname.blob.core.windows.net/sortedsv=2023-01-03&st=2025-06-18T08%3a45%3a11Z&se=2025-06-19T08%3a45%3a11Z&sr=c&sp=r&sig=ow2a1XbXmD4%2BEv9LBUkek8R%2FrAjvrQFpenUbzztILn8%3D"},
+               },
+               {
+                       // A SAS token assigned to a short lowercase variable 
(`sas=sp=r&...`)
+                       // must still be captured: the parameter run has to 
start at `sp`, not
+                       // be absorbed from the preceding `sas=` key.
+                       name: "valid pattern preceded by lowercase variable 
assignment",
+                       input: `
+       
sas=sp=r&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D
+       
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity
+       `,
+                       want: 
[]string{"https://trufflesecurity.blob.core.windows.net/trufflesecuritysp=r&st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c&sig=WSdF9YeZhvrbs%2B%2B1f8ZdDBzEe7fBJ%2BenuaXQ%2BJ9WOw0%3D"},
+               },
+               {
+                       name: "non-sas query string is ignored",
+                       input: `
+       AZURE_BLOB_QUERY=comp=list&restype=container&maxresults=100
+       
AZURE_BLOB_SAS_URL=https://trufflesecurity.blob.core.windows.net/trufflesecurity
+       `,
+                       want: nil,
+               },
+               {
                        name: "invalid pattern",
                        input: `
        
AZURE_BLOB_SAS_TOKEN=st=2025-03-04T07:24:52Z&se=2025-04-04T15:24:52Z&spr=https&sv=2022-11-02&sr=c
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/pkg/detectors/grafana/grafana.go 
new/trufflehog-3.95.8/pkg/detectors/grafana/grafana.go
--- old/trufflehog-3.95.7/pkg/detectors/grafana/grafana.go      2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/detectors/grafana/grafana.go      2026-07-02 
20:52:07.000000000 +0200
@@ -96,16 +96,38 @@
        }()
 
        switch resp.StatusCode {
-       case http.StatusOK:
+       case http.StatusOK, http.StatusForbidden:
+               // 200: token is valid and has permission to list tokens.
+               // 403: token is valid (authenticated) but lacks permission for 
this
+               // resource. Either way the credentials are genuine, so it's 
verified.
                return true, nil
        case http.StatusUnauthorized:
+               // Grafana returns 401 for two very different cases that can 
only be
+               // told apart by the response body:
+               //
+               //   1. A valid token that authenticated successfully but lacks 
the
+               //      accesspolicies:read scope. The body reports a 
permission/scope
+               //      error, e.g.:
+               //        {"code":"Unauthorized","message":"invalid permission: 
access
+               //         policy missing required scope [accesspolicies:read], 
..."}
+               //
+               //   2. An invalid/revoked token that failed authentication, 
e.g.:
+               //        {"code":"InvalidCredentials","message":"Token could 
not be parsed"}
+               //
+               // A permission/scope error can only be produced after 
authentication
+               // succeeds, so use that as the positive signal. We 
deliberately do not
+               // match the looser "Unauthorized" string because revoked 
tokens can
+               // also surface it, which previously caused false positives.
                bodyBytes, err := io.ReadAll(resp.Body)
                if err != nil {
                        return false, err
                }
+               body := string(bodyBytes)
 
-               // token is valid but has restricted permissions
-               return strings.Contains(string(bodyBytes), "Unauthorized"), nil
+               if strings.Contains(body, "invalid permission") || 
strings.Contains(body, "required scope") {
+                       return true, nil
+               }
+               return false, nil
        default:
                return false, fmt.Errorf("unexpected status code: %d", 
resp.StatusCode)
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/detectors/grafana/grafana_test.go 
new/trufflehog-3.95.8/pkg/detectors/grafana/grafana_test.go
--- old/trufflehog-3.95.7/pkg/detectors/grafana/grafana_test.go 2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/detectors/grafana/grafana_test.go 2026-07-02 
20:52:07.000000000 +0200
@@ -2,10 +2,12 @@
 
 import (
        "context"
+       "fmt"
        "testing"
 
        "github.com/google/go-cmp/cmp"
 
+       "github.com/trufflesecurity/trufflehog/v3/pkg/common"
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
        "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
 )
@@ -85,4 +87,85 @@
                        }
                })
        }
+}
+
+func TestGrafana_Verification(t *testing.T) {
+       tests := []struct {
+               name         string
+               statusCode   int
+               body         string
+               wantVerified bool
+               wantErr      bool
+       }{
+               {
+                       name:         "200 OK is verified",
+                       statusCode:   200,
+                       body:         `[{"id":"abc"}]`,
+                       wantVerified: true,
+               },
+               {
+                       name:         "403 Forbidden (valid token, restricted 
permissions) is verified",
+                       statusCode:   403,
+                       body:         `{"message":"You'll need additional 
permissions to perform this action."}`,
+                       wantVerified: true,
+               },
+               {
+                       // Valid token that authenticated but lacks 
accesspolicies:read scope.
+                       // Grafana returns 401 with a permission/scope error -> 
verified.
+                       name:         "401 with permission/scope error (valid 
token) is verified",
+                       statusCode:   401,
+                       body:         
`{"code":"Unauthorized","message":"invalid permission: access policy missing 
required scope [accesspolicies:read], received 
[accesspolicies:write]","requestId":"fc800c18-fb65-4013-bb8b-2e047a698974"}`,
+                       wantVerified: true,
+               },
+               {
+                       // Invalid/revoked token that failed authentication -> 
unverified.
+                       name:         "401 InvalidCredentials (revoked token) 
is unverified",
+                       statusCode:   401,
+                       body:         
`{"code":"InvalidCredentials","message":"Token could not be 
parsed","requestId":"538c7b37-2882-4727-9fae-6fc87321aa5c"}`,
+                       wantVerified: false,
+               },
+               {
+                       // Regression: a revoked token whose body merely 
contains
+                       // "Unauthorized" (but no permission/scope error) must 
NOT be verified.
+                       name:         "401 generic Unauthorized (revoked token) 
is unverified",
+                       statusCode:   401,
+                       body:         
`{"code":"Unauthorized","message":"Unauthorized"}`,
+                       wantVerified: false,
+               },
+               {
+                       name:         "401 with empty body is unverified",
+                       statusCode:   401,
+                       body:         ``,
+                       wantVerified: false,
+               },
+               {
+                       name:         "unexpected status code returns 
verification error",
+                       statusCode:   404,
+                       body:         ``,
+                       wantVerified: false,
+                       wantErr:      true,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       s := Scanner{client: 
common.ConstantResponseHttpClient(test.statusCode, test.body)}
+                       data := []byte(fmt.Sprintf("a grafana secret %s here", 
secret))
+
+                       results, err := s.FromData(context.Background(), true, 
data)
+                       if err != nil {
+                               t.Fatalf("FromData() unexpected error = %v", 
err)
+                       }
+                       if len(results) != 1 {
+                               t.Fatalf("expected 1 result, got %d", 
len(results))
+                       }
+
+                       if results[0].Verified != test.wantVerified {
+                               t.Errorf("Verified = %v, want %v", 
results[0].Verified, test.wantVerified)
+                       }
+                       if gotErr := results[0].VerificationError() != nil; 
gotErr != test.wantErr {
+                               t.Errorf("verification error present = %v, want 
%v (err = %v)", gotErr, test.wantErr, results[0].VerificationError())
+                       }
+               })
+       }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/pkg/detectors/jwt/jwt.go 
new/trufflehog-3.95.8/pkg/detectors/jwt/jwt.go
--- old/trufflehog-3.95.7/pkg/detectors/jwt/jwt.go      2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/detectors/jwt/jwt.go      2026-07-02 
20:52:07.000000000 +0200
@@ -16,6 +16,7 @@
        regexp "github.com/wasilibs/go-re2"
 
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/feature"
        "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
 )
 
@@ -78,6 +79,7 @@
        client := detectors.DetectorHttpClientWithNoLocalAddresses
 
        seenMatches := make(map[string]struct{})
+       skipUnverified := feature.DropUnverifiedJWTResults.Load()
 
        for _, matchGroups := range keyPat.FindAllStringSubmatch(string(data), 
-1) {
                match := matchGroups[1]
@@ -144,6 +146,10 @@
                        s1.SetVerificationError(verificationErr, match)
                }
 
+               // Remove unverified results from jwt detector output when the 
"drop-unverified-jwt-results" feature flag is enabled.
+               if skipUnverified && verify && !s1.Verified && 
s1.VerificationError() == nil {
+                       continue
+               }
                results = append(results, s1)
        }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/pkg/detectors/jwt/jwt_test.go 
new/trufflehog-3.95.8/pkg/detectors/jwt/jwt_test.go
--- old/trufflehog-3.95.7/pkg/detectors/jwt/jwt_test.go 2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/detectors/jwt/jwt_test.go 2026-07-02 
20:52:07.000000000 +0200
@@ -9,6 +9,7 @@
 
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
        "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/feature"
 )
 
 // This tests the JWT detector for a number of different cases (mostly 
HMAC-based) without verification enabled.
@@ -111,6 +112,72 @@
                        require.NoError(t, err)
 
                        if len(results) != len(test.want) {
+                               t.Errorf("mismatch in result count: expected 
%d, got %d", len(test.want), len(results))
+                               return
+                       }
+
+                       actual := make(map[string]struct{}, len(results))
+                       for _, r := range results {
+                               if len(r.RawV2) > 0 {
+                                       actual[string(r.RawV2)] = struct{}{}
+                               } else {
+                                       actual[string(r.Raw)] = struct{}{}
+                               }
+                       }
+
+                       expected := make(map[string]struct{}, len(test.want))
+                       for _, v := range test.want {
+                               expected[v] = struct{}{}
+                       }
+
+                       if diff := cmp.Diff(expected, actual); diff != "" {
+                               t.Errorf("%s diff: (-want +got)\n%s", 
test.name, diff)
+                       }
+               })
+       }
+}
+
+func TestJwt_SkipUnverified(t *testing.T) {
+       d := Scanner{}
+       ahoCorasickCore := 
ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
+       tests := []struct {
+               name           string
+               input          string
+               want           []string
+               skipUnverified bool
+       }{
+               {
+                       name:           "expired token - results should be 
skipped when skipUnverified is true",
+                       input:          
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTcwNDA2NzIwMCwiZXhwIjoxNzA0MDcwODAwfQ.9z15Ku3lWBC8rFPlvxkX3wx6wRalhoN7PlBRtE7fUXyoHqVLiZG2l5eMo6KwBlAu1L2hFtkvQwiJ79G5o3a4Bw",
+                       want:           []string{},
+                       skipUnverified: true,
+               },
+               {
+                       name:           "expired token - results should not be 
skipped when skipUnverified is false",
+                       input:          
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTcwNDA2NzIwMCwiZXhwIjoxNzA0MDcwODAwfQ.9z15Ku3lWBC8rFPlvxkX3wx6wRalhoN7PlBRtE7fUXyoHqVLiZG2l5eMo6KwBlAu1L2hFtkvQwiJ79G5o3a4Bw",
+                       want:           
[]string{"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTcwNDA2NzIwMCwiZXhwIjoxNzA0MDcwODAwfQ.9z15Ku3lWBC8rFPlvxkX3wx6wRalhoN7PlBRtE7fUXyoHqVLiZG2l5eMo6KwBlAu1L2hFtkvQwiJ79G5o3a4Bw"},
+                       skipUnverified: false,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       matchedDetectors := 
ahoCorasickCore.FindDetectorMatches([]byte(test.input))
+                       if len(matchedDetectors) == 0 {
+                               t.Errorf("test %q failed: expected keywords %v 
to be found in the input", test.name, d.Keywords())
+                               return
+                       }
+
+                       if test.skipUnverified {
+                               feature.DropUnverifiedJWTResults.Store(true)
+                       } else {
+                               feature.DropUnverifiedJWTResults.Store(false)
+                       }
+
+                       results, err := d.FromData(context.Background(), true, 
[]byte(test.input))
+                       require.NoError(t, err)
+
+                       if len(results) != len(test.want) {
                                t.Errorf("mismatch in result count: expected 
%d, got %d", len(test.want), len(results))
                                return
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/detectors/octopusdeploy/octopusdeploy.go 
new/trufflehog-3.95.8/pkg/detectors/octopusdeploy/octopusdeploy.go
--- old/trufflehog-3.95.7/pkg/detectors/octopusdeploy/octopusdeploy.go  
1970-01-01 01:00:00.000000000 +0100
+++ new/trufflehog-3.95.8/pkg/detectors/octopusdeploy/octopusdeploy.go  
2026-07-02 20:52:07.000000000 +0200
@@ -0,0 +1,154 @@
+package octopusdeploy
+
+import (
+       "context"
+       "fmt"
+       "io"
+       "net/http"
+       "net/url"
+
+       regexp "github.com/wasilibs/go-re2"
+
+       "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
+)
+
+type Scanner struct {
+       client *http.Client
+       detectors.DefaultMultiPartCredentialProvider
+}
+
+// Compile-time interface check
+var _ detectors.Detector = (*Scanner)(nil)
+
+var (
+       defaultClient = 
detectors.NewClientWithDedup(detectors.DetectorHttpClientWithNoLocalAddresses)
+
+       // Octopus Deploy API keys:
+       // Format: API- followed by 29–34 uppercase letters or digits
+       octopusTokenPat = regexp.MustCompile(
+               `\b(API-[A-Z0-9]{29,34})\b`,
+       )
+
+       urlPat = regexp.MustCompile(`\b([a-z0-9-]+\.octopus\.app)\b`)
+)
+
+// Keywords used for fast pre-filtering
+func (s Scanner) Keywords() []string {
+       return []string{"octopus"}
+}
+
+func (s Scanner) getClient() *http.Client {
+       if s.client != nil {
+               return s.client
+       }
+       return defaultClient
+}
+
+// FromData scans for Octopus API tokens and optionally verifies them
+func (s Scanner) FromData(
+       ctx context.Context,
+       verify bool,
+       data []byte,
+) (results []detectors.Result, err error) {
+
+       dataStr := string(data)
+
+       uniqueTokens := make(map[string]struct{})
+       uniqueUrls := make(map[string]struct{})
+
+       for _, urlMatch := range urlPat.FindAllStringSubmatch(dataStr, -1) {
+               uniqueUrls[urlMatch[1]] = struct{}{}
+       }
+       for _, match := range octopusTokenPat.FindAllStringSubmatch(dataStr, 
-1) {
+               uniqueTokens[match[1]] = struct{}{}
+       }
+
+       for url := range uniqueUrls {
+               for token := range uniqueTokens {
+                       result := detectors.Result{
+                               DetectorType: 
detector_typepb.DetectorType_OctopusDeploy,
+                               Raw:          []byte(token),
+                               RawV2:        []byte(fmt.Sprintf("%s:%s", url, 
token)),
+                               Redacted:     token[:8] + "...",
+                               SecretParts: map[string]string{
+                                       "key": token,
+                                       "url": url,
+                               },
+                       }
+
+                       if verify {
+                               verified, verificationErr := verifyOctopusToken(
+                                       ctx,
+                                       s.getClient(),
+                                       url,
+                                       token,
+                               )
+                               result.SetVerificationError(verificationErr, 
token)
+                               result.Verified = verified
+                       }
+
+                       results = append(results, result)
+               }
+       }
+
+       return
+}
+
+func verifyOctopusToken(
+       ctx context.Context,
+       client *http.Client,
+       baseUrl string,
+       token string,
+) (bool, error) {
+       // API REFERENCE: https://trufflesec.octopus.app/api
+       // DOCS: https://octopus.com/docs/octopus-rest-api
+       url := &url.URL{
+               Scheme: "https",
+               Host:   baseUrl,
+               Path:   "/api/users/me",
+       }
+       req, err := http.NewRequestWithContext(
+               ctx,
+               http.MethodGet,
+               url.String(),
+               http.NoBody,
+       )
+       if err != nil {
+               return false, err
+       }
+
+       req.Header.Set("X-Octopus-ApiKey", token)
+
+       res, err := detectors.DoWithDedup(client, 
detector_typepb.DetectorType_OctopusDeploy, fmt.Sprintf("%s:%s", baseUrl, 
token), req)
+       if err != nil {
+               return false, err
+       }
+       defer func() {
+               _, _ = io.Copy(io.Discard, res.Body)
+               _ = res.Body.Close()
+       }()
+
+       switch res.StatusCode {
+       case http.StatusOK, http.StatusForbidden:
+               return true, nil
+
+       case http.StatusUnauthorized:
+               // Invalid or revoked key
+               return false, nil
+
+       default:
+               return false, fmt.Errorf(
+                       "unexpected HTTP response status %d",
+                       res.StatusCode,
+               )
+       }
+}
+
+func (s Scanner) Type() detector_typepb.DetectorType {
+       return detector_typepb.DetectorType_OctopusDeploy
+}
+
+func (s Scanner) Description() string {
+       return "Octopus Deploy is a DevOps deployment automation platform. 
Octopus Deploy API keys can be used to automate deployments, manage projects, 
environments, and infrastructure resources."
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/detectors/octopusdeploy/octopusdeploy_integration_test.go
 
new/trufflehog-3.95.8/pkg/detectors/octopusdeploy/octopusdeploy_integration_test.go
--- 
old/trufflehog-3.95.7/pkg/detectors/octopusdeploy/octopusdeploy_integration_test.go
 1970-01-01 01:00:00.000000000 +0100
+++ 
new/trufflehog-3.95.8/pkg/detectors/octopusdeploy/octopusdeploy_integration_test.go
 2026-07-02 20:52:07.000000000 +0200
@@ -0,0 +1,206 @@
+//go:build detectors
+// +build detectors
+
+package octopusdeploy
+
+import (
+       "context"
+       "fmt"
+       "testing"
+       "time"
+
+       "github.com/google/go-cmp/cmp"
+       "github.com/google/go-cmp/cmp/cmpopts"
+
+       "github.com/trufflesecurity/trufflehog/v3/pkg/common"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
+)
+
+func TestOctopusDeploy_FromData(t *testing.T) {
+       ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+
+       // Expect secrets structured like:
+       // OCTOPUS_CLOUD_URL = acme.octopus.app
+       // OCTOPUS_API_KEY = API-XXXXXXXXXXXXXXXXXXXXXXXXXXXX
+       testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", 
"detectors6")
+       if err != nil {
+               t.Fatalf("could not get test secrets from GCP: %s", err)
+       }
+
+       baseURL := testSecrets.MustGetField("OCTOPUS_CLOUD_URL")
+       activeToken := testSecrets.MustGetField("OCTOPUS_API_KEY")
+
+       inactiveToken := "API-AAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+
+       type args struct {
+               ctx    context.Context
+               data   []byte
+               verify bool
+       }
+
+       tests := []struct {
+               name                string
+               s                   Scanner
+               args                args
+               want                []detectors.Result
+               wantErr             bool
+               wantVerificationErr bool
+       }{
+               {
+                       name: "found, verified",
+                       s:    Scanner{},
+                       args: args{
+                               ctx: context.Background(),
+                               data: fmt.Appendf(
+                                       []byte{},
+                                       "Server %s using key %s",
+                                       baseURL,
+                                       activeToken,
+                               ),
+                               verify: true,
+                       },
+                       want: []detectors.Result{
+                               {
+                                       DetectorType: 
detector_typepb.DetectorType_OctopusDeploy,
+                                       Verified:     true,
+                                       Raw:          []byte(activeToken),
+                                       RawV2:        
[]byte(fmt.Sprintf("%s:%s", baseURL, activeToken)),
+                               },
+                       },
+               },
+               {
+                       name: "found, real token, verification timeout",
+                       s:    Scanner{client: common.SaneHttpClientTimeOut(1 * 
time.Microsecond)},
+                       args: args{
+                               ctx: context.Background(),
+                               data: fmt.Appendf(
+                                       []byte{},
+                                       "%s %s",
+                                       baseURL,
+                                       activeToken,
+                               ),
+                               verify: true,
+                       },
+                       want: []detectors.Result{
+                               {
+                                       DetectorType: 
detector_typepb.DetectorType_OctopusDeploy,
+                                       Verified:     false,
+                                       Raw:          []byte(activeToken),
+                                       RawV2:        
[]byte(fmt.Sprintf("%s:%s", baseURL, activeToken)),
+                               },
+                       },
+                       wantVerificationErr: true,
+               },
+               {
+                       name: "found, real token, unexpected api response",
+                       s:    Scanner{client: 
common.ConstantResponseHttpClient(500, "{}")},
+                       args: args{
+                               ctx: context.Background(),
+                               data: fmt.Appendf(
+                                       []byte{},
+                                       "%s %s",
+                                       baseURL,
+                                       activeToken,
+                               ),
+                               verify: true,
+                       },
+                       want: []detectors.Result{
+                               {
+                                       DetectorType: 
detector_typepb.DetectorType_OctopusDeploy,
+                                       Verified:     false,
+                                       Raw:          []byte(activeToken),
+                                       RawV2:        
[]byte(fmt.Sprintf("%s:%s", baseURL, activeToken)),
+                               },
+                       },
+                       wantVerificationErr: true,
+               },
+               {
+                       name: "found, unverified (inactive token)",
+                       s:    Scanner{},
+                       args: args{
+                               ctx: context.Background(),
+                               data: fmt.Appendf(
+                                       []byte{},
+                                       "%s %s",
+                                       baseURL,
+                                       inactiveToken,
+                               ),
+                               verify: true,
+                       },
+                       want: []detectors.Result{
+                               {
+                                       DetectorType: 
detector_typepb.DetectorType_OctopusDeploy,
+                                       Verified:     false,
+                                       Raw:          []byte(inactiveToken),
+                                       RawV2:        
[]byte(fmt.Sprintf("%s:%s", baseURL, inactiveToken)),
+                               },
+                       },
+               },
+               {
+                       name: "not found",
+                       s:    Scanner{},
+                       args: args{
+                               ctx:    context.Background(),
+                               data:   []byte("no secrets here"),
+                               verify: true,
+                       },
+                       want: nil,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, 
tt.args.data)
+                       if (err != nil) != tt.wantErr {
+                               t.Fatalf("OctopusDeploy.FromData() error = %v, 
wantErr %v", err, tt.wantErr)
+                       }
+
+                       for i := range got {
+                               if len(got[i].Raw) == 0 {
+                                       t.Fatal("no raw secret present")
+                               }
+                               if (got[i].VerificationError() != nil) != 
tt.wantVerificationErr {
+                                       t.Fatalf(
+                                               "wantVerificationError = %v, 
verification error = %v",
+                                               tt.wantVerificationErr,
+                                               got[i].VerificationError(),
+                                       )
+                               }
+                       }
+
+                       ignoreOpts := cmpopts.IgnoreFields(
+                               detectors.Result{},
+                               "ExtraData",
+                               "verificationError",
+                               "primarySecret",
+                               "Redacted",
+                               "chunkOffset",
+                               "chunkOffsetSet",
+                               "SecretParts",
+                       )
+
+                       if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != 
"" {
+                               t.Errorf("OctopusDeploy.FromData() %s diff: 
(-got +want)\n%s", tt.name, diff)
+                       }
+               })
+       }
+}
+
+func BenchmarkOctopusDeploy_FromData(b *testing.B) {
+       ctx := context.Background()
+       s := Scanner{}
+
+       for name, data := range detectors.MustGetBenchmarkData() {
+               b.Run(name, func(b *testing.B) {
+                       b.ResetTimer()
+                       for n := 0; n < b.N; n++ {
+                               _, err := s.FromData(ctx, false, data)
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                       }
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/detectors/octopusdeploy/octopusdeploy_test.go 
new/trufflehog-3.95.8/pkg/detectors/octopusdeploy/octopusdeploy_test.go
--- old/trufflehog-3.95.7/pkg/detectors/octopusdeploy/octopusdeploy_test.go     
1970-01-01 01:00:00.000000000 +0100
+++ new/trufflehog-3.95.8/pkg/detectors/octopusdeploy/octopusdeploy_test.go     
2026-07-02 20:52:07.000000000 +0200
@@ -0,0 +1,192 @@
+package octopusdeploy
+
+import (
+       "context"
+       "testing"
+
+       "github.com/google/go-cmp/cmp"
+       "github.com/stretchr/testify/require"
+
+       "github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
+)
+
+func TestOctopusDeploy_Pattern(t *testing.T) {
+       d := Scanner{}
+       ahoCorasickCore := 
ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
+
+       tests := []struct {
+               name  string
+               input string
+               want  []string
+       }{
+               {
+                       name: "valid pattern",
+                       input: `
+                               func setupOctopusClient() (*http.Client, error) 
{
+                                       baseUrl := &url.URL{Scheme: "https", 
Host: "acme.octopus.app", Path: "/api/users/me"}
+
+                                       // Create a new request with the API 
key as a header
+                                       req, err := http.NewRequest("GET", 
baseUrl.String(), http.NoBody)
+                                       if err != nil {
+                                               return nil, err
+                                       }
+                                       req.Header.Set("X-Octopus-ApiKey", 
"API-1234567890ABCDEFGHIJKLMNO1234")
+
+                                       client := &http.Client{}
+                                       resp, _ := client.Do(req)
+                                       defer func() { _ = resp.Body.Close() }()
+
+                                       return client, nil
+                               }
+                               `,
+                       want: []string{
+                               
"acme.octopus.app:API-1234567890ABCDEFGHIJKLMNO1234",
+                       },
+               },
+               {
+                       name: "valid pattern - env file",
+                       input: `
+                               # .env.production
+                               APP_NAME=deploy-bot
+                               OCTOPUS_URL=prod.octopus.app
+                               
OCTOPUS_API_KEY=API-AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+                               LOG_LEVEL=info
+                               `,
+                       want: []string{
+                               
"prod.octopus.app:API-AAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+                       },
+               },
+               {
+                       name: "valid pattern - multiple tokens",
+                       input: `
+                               func loadOctopusKeys() []string {
+                                       // Rotated keys retained during the 
grace period
+                                       return []string{
+                                               
"API-11111111111111111111111111111",
+                                               
"API-22222222222222222222222222222",
+                                       }
+                               }
+
+                               // deployments run against dev.octopus.app
+                               `,
+                       want: []string{
+                               
"dev.octopus.app:API-11111111111111111111111111111",
+                               
"dev.octopus.app:API-22222222222222222222222222222",
+                       },
+               },
+               {
+                       name: "valid pattern - multiple urls and tokens",
+                       input: `
+                               environments:
+                                       - name: staging
+                                               url: acme.octopus.app
+                                       - name: production
+                                               url: prod.octopus.app
+
+                               # shared deployment key used across environments
+                               api_key: API-AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+                               `,
+                       want: []string{
+                               
"acme.octopus.app:API-AAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+                               
"prod.octopus.app:API-AAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+                       },
+               },
+               {
+                       name: "invalid pattern - lowercase token",
+                       input: `
+                               func setupOctopusClient() {
+                                       baseUrl := "acme.octopus.app"
+                                       // Key was accidentally lowercased 
during a config export
+                                       apiKey := 
"API-abcdefghijklmnopqrstuvwxyz1234"
+                                       client.SetApiKey(baseUrl, apiKey)
+                               }
+                               `,
+                       want: nil,
+               },
+               {
+                       name: "invalid pattern - too short",
+                       input: `
+                               func setupOctopusClient() {
+                                       baseUrl := "acme.octopus.app"
+                                       // Truncated key accidentally committed
+                                       apiKey := "API-1234"
+                                       client.SetApiKey(baseUrl, apiKey)
+                               }
+                               `,
+                       want: nil,
+               },
+               {
+                       name: "invalid pattern - too long",
+                       input: `
+                               func setupOctopusClient() {
+                                       baseUrl := "acme.octopus.app"
+                                       // Extra characters appended by a bad 
find-and-replace
+                                       apiKey := 
"API-ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
+                                       client.SetApiKey(baseUrl, apiKey)
+                               }
+                               `,
+                       want: nil,
+               },
+               {
+                       name: "invalid pattern - url only",
+                       input: `
+                               func octopusHealthCheck() error {
+                                       resp, err := 
http.Get("https://acme.octopus.app/api/status";)
+                                       if err != nil {
+                                               return err
+                                       }
+                                       defer func() { _ = resp.Body.Close() }()
+                                       return nil
+                               }
+                               `,
+                       want: nil,
+               },
+               {
+                       name: "invalid pattern - token only",
+                       input: `
+                               // TODO: load the real key instead of 
octopus_token env var
+                               func isOctopusToken(s string) bool {
+                                       return strings.HasPrefix(s, 
"API-AAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
+                               }
+                               `,
+                       want: nil,
+               },
+       }
+
+       for _, test := range tests {
+               t.Run(test.name, func(t *testing.T) {
+                       matchedDetectors := 
ahoCorasickCore.FindDetectorMatches([]byte(test.input))
+                       if len(matchedDetectors) == 0 {
+                               t.Errorf("test %q failed: expected keywords %v 
to be found in the input", test.name, d.Keywords())
+                               return
+                       }
+
+                       results, err := d.FromData(context.Background(), false, 
[]byte(test.input))
+                       require.NoError(t, err)
+
+                       if len(results) != len(test.want) {
+                               t.Errorf(
+                                       "mismatch in result count: expected %d, 
got %d",
+                                       len(test.want),
+                                       len(results),
+                               )
+                               return
+                       }
+
+                       actual := make(map[string]struct{}, len(results))
+                       for _, r := range results {
+                               actual[string(r.RawV2)] = struct{}{}
+                       }
+
+                       expected := make(map[string]struct{}, len(test.want))
+                       for _, v := range test.want {
+                               expected[v] = struct{}{}
+                       }
+
+                       if diff := cmp.Diff(expected, actual); diff != "" {
+                               t.Errorf("%s diff: (-want +got)\n%s", 
test.name, diff)
+                       }
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/pkg/engine/defaults/defaults.go 
new/trufflehog-3.95.8/pkg/engine/defaults/defaults.go
--- old/trufflehog-3.95.7/pkg/engine/defaults/defaults.go       2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/engine/defaults/defaults.go       2026-07-02 
20:52:07.000000000 +0200
@@ -523,6 +523,7 @@
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nvapi"
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/nylas"
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/oanda"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/octopusdeploy"
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/okta"
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/omnisend"
        "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/onedesk"
@@ -1423,6 +1424,7 @@
                &nvapi.Scanner{},
                &nylas.Scanner{},
                &oanda.Scanner{},
+               &octopusdeploy.Scanner{},
                &okta.Scanner{},
                &omnisend.Scanner{},
                &onedesk.Scanner{},
@@ -1812,6 +1814,8 @@
                        return !feature.PgAnalyzeReadKeyDetectorEnabled.Load()
                case *redhatpyxis.Scanner:
                        return !feature.RedHatPyxisDetectorEnabled.Load()
+               case *octopusdeploy.Scanner:
+                       return !feature.OctopusDeployDetectorEnabled.Load()
                default:
                        return false
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/engine/defaults/defaults_test.go 
new/trufflehog-3.95.8/pkg/engine/defaults/defaults_test.go
--- old/trufflehog-3.95.7/pkg/engine/defaults/defaults_test.go  2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/engine/defaults/defaults_test.go  2026-07-02 
20:52:07.000000000 +0200
@@ -137,6 +137,7 @@
        detector_typepb.DetectorType_BrainTrustApiKey: {},
        detector_typepb.DetectorType_PgAnalyzeReadKey: {},
        detector_typepb.DetectorType_RedHatPyxis:      {},
+       detector_typepb.DetectorType_OctopusDeploy:    {},
 
        // Reserved / special types.
        detector_typepb.DetectorType_CustomRegex: {}, // added dynamically via 
engine config, not via buildDetectorList()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/pkg/engine/engine.go 
new/trufflehog-3.95.8/pkg/engine/engine.go
--- old/trufflehog-3.95.7/pkg/engine/engine.go  2026-06-29 12:17:14.000000000 
+0200
+++ new/trufflehog-3.95.8/pkg/engine/engine.go  2026-07-02 20:52:07.000000000 
+0200
@@ -232,6 +232,10 @@
        verificationOverlapWorkerMultiplier int
 
        maxDecodeDepth int
+
+       // runtimeCollector exposes live channel/worker/scan counters to 
Prometheus
+       // while the engine is running. Set in Start, cleared in Finish.
+       runtimeCollector *runtimeCollector
 }
 
 // NewEngine creates a new Engine instance with the provided configuration.
@@ -639,6 +643,7 @@
 func (e *Engine) Start(ctx context.Context) {
        e.metrics = runtimeMetrics{Metrics: Metrics{scanStartTime: time.Now()}}
        e.sanityChecks(ctx)
+       e.registerRuntimeMetrics(ctx)
        e.startWorkers(ctx)
 }
 
@@ -756,6 +761,8 @@
 
        e.metrics.ScanDuration = time.Since(e.metrics.scanStartTime)
 
+       e.unregisterRuntimeMetrics()
+
        return err
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/pkg/engine/runtime_collector.go 
new/trufflehog-3.95.8/pkg/engine/runtime_collector.go
--- old/trufflehog-3.95.7/pkg/engine/runtime_collector.go       1970-01-01 
01:00:00.000000000 +0100
+++ new/trufflehog-3.95.8/pkg/engine/runtime_collector.go       2026-07-02 
20:52:07.000000000 +0200
@@ -0,0 +1,201 @@
+package engine
+
+import (
+       "errors"
+       "sync/atomic"
+
+       "github.com/prometheus/client_golang/prometheus"
+
+       "github.com/trufflesecurity/trufflehog/v3/pkg/common"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/context"
+)
+
+// Channel and worker pool label values exposed by runtimeCollector. These
+// names appear in metric labels, so external dashboards depend on them being
+// stable.
+const (
+       channelSourceChunks              = "source_chunks_chan"
+       channelDetectableChunks          = "detectable_chunks_chan"
+       channelVerificationOverlapChunks = "verification_overlap_chunks_chan"
+       channelResults                   = "results_chan"
+
+       workerEngine              = "engine_workers"
+       workerDetector            = "detector_workers"
+       workerVerificationOverlap = "verification_overlap_workers"
+       workerNotifier            = "notifier_workers"
+       workerSources             = "source_workers"
+)
+
+// runtimeCollector exposes live engine internals (channel queue depths,
+// channel capacities, configured worker pool sizes, and aggregate scan
+// counters) to Prometheus. Values are sampled lazily on each scrape via
+// len/cap and atomic loads, so there is no background goroutine overhead
+// between scrapes.
+type runtimeCollector struct {
+       engine *Engine
+
+       channelSize     *prometheus.Desc
+       channelCapacity *prometheus.Desc
+       workerCount     *prometheus.Desc
+       activeSources   *prometheus.Desc
+
+       bytesScanned           *prometheus.Desc
+       chunksScanned          *prometheus.Desc
+       verifiedSecretsFound   *prometheus.Desc
+       unverifiedSecretsFound *prometheus.Desc
+}
+
+// newRuntimeCollector constructs a runtimeCollector bound to the given
+// Engine. The metric descriptors are built once here; each subsequent Collect
+// call reuses them and only samples fresh values.
+func newRuntimeCollector(e *Engine) *runtimeCollector {
+       fq := func(name string) string {
+               return prometheus.BuildFQName(common.MetricsNamespace, 
common.MetricsSubsystem, name)
+       }
+       return &runtimeCollector{
+               engine: e,
+               channelSize: prometheus.NewDesc(
+                       fq("engine_channel_size"),
+                       "Current number of items buffered in an engine 
channel.",
+                       []string{"channel"}, nil,
+               ),
+               channelCapacity: prometheus.NewDesc(
+                       fq("engine_channel_capacity"),
+                       "Buffer capacity of an engine channel.",
+                       []string{"channel"}, nil,
+               ),
+               workerCount: prometheus.NewDesc(
+                       fq("engine_worker_count"),
+                       "Configured number of workers in an engine worker 
pool.",
+                       []string{"worker_type"}, nil,
+               ),
+               activeSources: prometheus.NewDesc(
+                       fq("engine_active_sources"),
+                       "Number of sources currently running.",
+                       nil, nil,
+               ),
+               bytesScanned: prometheus.NewDesc(
+                       fq("engine_bytes_scanned"),
+                       "Total bytes scanned by the engine since Start.",
+                       nil, nil,
+               ),
+               chunksScanned: prometheus.NewDesc(
+                       fq("engine_chunks_scanned"),
+                       "Total chunks scanned by the engine since Start.",
+                       nil, nil,
+               ),
+               verifiedSecretsFound: prometheus.NewDesc(
+                       fq("engine_verified_secrets_found"),
+                       "Total verified secrets found by the engine since 
Start.",
+                       nil, nil,
+               ),
+               unverifiedSecretsFound: prometheus.NewDesc(
+                       fq("engine_unverified_secrets_found"),
+                       "Total unverified secrets found by the engine since 
Start.",
+                       nil, nil,
+               ),
+       }
+}
+
+// Describe implements prometheus.Collector by emitting every metric
+// descriptor this collector will ever produce. The registry uses these
+// descriptors to detect conflicts at registration time.
+func (c *runtimeCollector) Describe(ch chan<- *prometheus.Desc) {
+       ch <- c.channelSize
+       ch <- c.channelCapacity
+       ch <- c.workerCount
+       ch <- c.activeSources
+       ch <- c.bytesScanned
+       ch <- c.chunksScanned
+       ch <- c.verifiedSecretsFound
+       ch <- c.unverifiedSecretsFound
+}
+
+// Collect implements prometheus.Collector and is invoked by the registry on
+// every scrape. It samples the engine's channels via len/cap, reads scan
+// counters via atomic.Load, and emits one metric per (descriptor, label set)
+// combination.
+func (c *runtimeCollector) Collect(ch chan<- prometheus.Metric) {
+       e := c.engine
+
+       sourceChunks := e.sourceManager.Chunks()
+       channels := []struct {
+               name     string
+               size     int
+               capacity int
+       }{
+               {channelSourceChunks, len(sourceChunks), cap(sourceChunks)},
+               {channelDetectableChunks, len(e.detectableChunksChan), 
cap(e.detectableChunksChan)},
+               {channelVerificationOverlapChunks, 
len(e.verificationOverlapChunksChan), cap(e.verificationOverlapChunksChan)},
+               {channelResults, len(e.results), cap(e.results)},
+       }
+       for _, q := range channels {
+               ch <- prometheus.MustNewConstMetric(c.channelSize, 
prometheus.GaugeValue, float64(q.size), q.name)
+               ch <- prometheus.MustNewConstMetric(c.channelCapacity, 
prometheus.GaugeValue, float64(q.capacity), q.name)
+       }
+
+       workers := []struct {
+               name  string
+               count int
+       }{
+               {workerEngine, e.concurrency},
+               {workerDetector, e.concurrency * e.detectorWorkerMultiplier},
+               {workerVerificationOverlap, e.concurrency * 
e.verificationOverlapWorkerMultiplier},
+               {workerNotifier, e.concurrency * 
e.notificationWorkerMultiplier},
+               // SourceManager treats its concurrency limit as a semaphore 
rather than a
+               // fixed worker pool, but reporting it here lets dashboards 
ratio it against
+               // activeSources to spot source-side saturation.
+               {workerSources, e.sourceManager.MaxConcurrentSources()},
+       }
+       for _, w := range workers {
+               ch <- prometheus.MustNewConstMetric(c.workerCount, 
prometheus.GaugeValue, float64(w.count), w.name)
+       }
+
+       ch <- prometheus.MustNewConstMetric(
+               c.activeSources, prometheus.GaugeValue,
+               float64(e.sourceManager.ConcurrentSources()),
+       )
+
+       // Scan counters are written via atomic.AddUint64 on runtimeMetrics; 
read
+       // them with atomic.LoadUint64 to stay cheap on scrape and avoid the RW 
mutex.
+       m := &e.metrics.Metrics
+       ch <- prometheus.MustNewConstMetric(c.bytesScanned, 
prometheus.CounterValue, float64(atomic.LoadUint64(&m.BytesScanned)))
+       ch <- prometheus.MustNewConstMetric(c.chunksScanned, 
prometheus.CounterValue, float64(atomic.LoadUint64(&m.ChunksScanned)))
+       ch <- prometheus.MustNewConstMetric(c.verifiedSecretsFound, 
prometheus.CounterValue, float64(atomic.LoadUint64(&m.VerifiedSecretsFound)))
+       ch <- prometheus.MustNewConstMetric(c.unverifiedSecretsFound, 
prometheus.CounterValue, float64(atomic.LoadUint64(&m.UnverifiedSecretsFound)))
+}
+
+// registerRuntimeMetrics installs the engine's runtime collector into the
+// default Prometheus registry. If a collector with identical descriptors is
+// already registered (e.g. a previous engine in the same process that didn't
+// call Finish), the stale collector is evicted and replaced. Without eviction
+// the stale collector would pin a dead engine in memory and permanently block
+// future engines from exposing metrics.
+func (e *Engine) registerRuntimeMetrics(ctx context.Context) {
+       collector := newRuntimeCollector(e)
+       err := prometheus.DefaultRegisterer.Register(collector)
+       if err != nil {
+               var already prometheus.AlreadyRegisteredError
+               if !errors.As(err, &already) {
+                       ctx.Logger().Error(err, "failed to register engine 
runtime metrics")
+                       return
+               }
+               ctx.Logger().V(2).Info("evicting stale engine runtime metrics 
collector")
+               
prometheus.DefaultRegisterer.Unregister(already.ExistingCollector)
+               if err := prometheus.DefaultRegisterer.Register(collector); err 
!= nil {
+                       ctx.Logger().Error(err, "failed to register engine 
runtime metrics after eviction")
+                       return
+               }
+       }
+       e.runtimeCollector = collector
+}
+
+// unregisterRuntimeMetrics removes the engine's runtime collector from the
+// default Prometheus registry. Safe to call when registration was skipped.
+func (e *Engine) unregisterRuntimeMetrics() {
+       if e.runtimeCollector == nil {
+               return
+       }
+       prometheus.DefaultRegisterer.Unregister(e.runtimeCollector)
+       e.runtimeCollector = nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/pkg/feature/feature.go 
new/trufflehog-3.95.8/pkg/feature/feature.go
--- old/trufflehog-3.95.7/pkg/feature/feature.go        2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/feature/feature.go        2026-07-02 
20:52:07.000000000 +0200
@@ -28,6 +28,8 @@
        BraintrustDetectorEnabled       atomic.Bool
        PgAnalyzeReadKeyDetectorEnabled atomic.Bool
        RedHatPyxisDetectorEnabled      atomic.Bool
+       OctopusDeployDetectorEnabled    atomic.Bool
+       DropUnverifiedJWTResults        atomic.Bool
 )
 
 type AtomicString struct {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/pb/detector_typepb/detector_type.pb.go 
new/trufflehog-3.95.8/pkg/pb/detector_typepb/detector_type.pb.go
--- old/trufflehog-3.95.7/pkg/pb/detector_typepb/detector_type.pb.go    
2026-06-29 12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/pb/detector_typepb/detector_type.pb.go    
2026-07-02 20:52:07.000000000 +0200
@@ -1109,6 +1109,7 @@
        DetectorType_BrainTrustApiKey                        DetectorType = 1053
        DetectorType_PgAnalyzeReadKey                        DetectorType = 1054
        DetectorType_RedHatPyxis                             DetectorType = 1055
+       DetectorType_OctopusDeploy                           DetectorType = 1056
 )
 
 // Enum value maps for DetectorType.
@@ -2166,6 +2167,7 @@
                1053: "BrainTrustApiKey",
                1054: "PgAnalyzeReadKey",
                1055: "RedHatPyxis",
+               1056: "OctopusDeploy",
        }
        DetectorType_value = map[string]int32{
                "Alibaba":                               0,
@@ -3220,6 +3222,7 @@
                "BrainTrustApiKey":                  1053,
                "PgAnalyzeReadKey":                  1054,
                "RedHatPyxis":                       1055,
+               "OctopusDeploy":                     1056,
        }
 )
 
@@ -3255,7 +3258,7 @@
 var file_detector_type_proto_rawDesc = []byte{
        0x0a, 0x13, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x74, 
0x79, 0x70, 0x65, 0x2e,
        0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x64, 0x65, 0x74, 0x65, 0x63, 
0x74, 0x6f, 0x72, 0x5f,
-       0x74, 0x79, 0x70, 0x65, 0x2a, 0xa2, 0x89, 0x01, 0x0a, 0x0c, 0x44, 0x65, 
0x74, 0x65, 0x63, 0x74,
+       0x74, 0x79, 0x70, 0x65, 0x2a, 0xb6, 0x89, 0x01, 0x0a, 0x0c, 0x44, 0x65, 
0x74, 0x65, 0x63, 0x74,
        0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x6c, 
0x69, 0x62, 0x61, 0x62,
        0x61, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4d, 0x51, 0x50, 0x10, 
0x01, 0x12, 0x07, 0x0a,
        0x03, 0x41, 0x57, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x7a, 
0x75, 0x72, 0x65, 0x10,
@@ -4353,12 +4356,13 @@
        0x61, 0x69, 0x6e, 0x54, 0x72, 0x75, 0x73, 0x74, 0x41, 0x70, 0x69, 0x4b, 
0x65, 0x79, 0x10, 0x9d,
        0x08, 0x12, 0x15, 0x0a, 0x10, 0x50, 0x67, 0x41, 0x6e, 0x61, 0x6c, 0x79, 
0x7a, 0x65, 0x52, 0x65,
        0x61, 0x64, 0x4b, 0x65, 0x79, 0x10, 0x9e, 0x08, 0x12, 0x10, 0x0a, 0x0b, 
0x52, 0x65, 0x64, 0x48,
-       0x61, 0x74, 0x50, 0x79, 0x78, 0x69, 0x73, 0x10, 0x9f, 0x08, 0x42, 0x41, 
0x5a, 0x3f, 0x67, 0x69,
-       0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x72, 0x75, 
0x66, 0x66, 0x6c, 0x65,
-       0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x75, 
0x66, 0x66, 0x6c, 0x65,
-       0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 
0x62, 0x2f, 0x64, 0x65,
-       0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x70, 
0x62, 0x62, 0x06, 0x70,
-       0x72, 0x6f, 0x74, 0x6f, 0x33,
+       0x61, 0x74, 0x50, 0x79, 0x78, 0x69, 0x73, 0x10, 0x9f, 0x08, 0x12, 0x12, 
0x0a, 0x0d, 0x4f, 0x63,
+       0x74, 0x6f, 0x70, 0x75, 0x73, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x10, 
0xa0, 0x08, 0x42, 0x41,
+       0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 
0x2f, 0x74, 0x72, 0x75,
+       0x66, 0x66, 0x6c, 0x65, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 
0x2f, 0x74, 0x72, 0x75,
+       0x66, 0x66, 0x6c, 0x65, 0x68, 0x6f, 0x67, 0x2f, 0x76, 0x33, 0x2f, 0x70, 
0x6b, 0x67, 0x2f, 0x70,
+       0x62, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x74, 
0x79, 0x70, 0x65, 0x70,
+       0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/sources/filesystem/filesystem.go 
new/trufflehog-3.95.8/pkg/sources/filesystem/filesystem.go
--- old/trufflehog-3.95.7/pkg/sources/filesystem/filesystem.go  2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/pkg/sources/filesystem/filesystem.go  2026-07-02 
20:52:07.000000000 +0200
@@ -42,9 +42,11 @@
 }
 
 // Ensure the Source satisfies the interfaces at compile time
-var _ sources.Source = (*Source)(nil)
-var _ sources.SourceUnitUnmarshaller = (*Source)(nil)
-var _ sources.SourceUnitEnumChunker = (*Source)(nil)
+var (
+       _ sources.Source                 = (*Source)(nil)
+       _ sources.SourceUnitUnmarshaller = (*Source)(nil)
+       _ sources.SourceUnitEnumChunker  = (*Source)(nil)
+)
 
 // max symlink depth allowed
 const defaultMaxSymlinkDepth = 40
@@ -116,7 +118,7 @@
                if common.IsDone(ctx) {
                        return nil
                }
-               s.SetProgressComplete(i, len(s.paths), fmt.Sprintf("Path: %s", 
rootPath), "")
+               s.SetProgressComplete(i, len(s.paths), fmt.Sprintf("Path: %s", 
rootPath), s.GetProgress().EncodedResumeInfo)
 
                cleanPath := filepath.Clean(rootPath)
                fileInfo, err := os.Lstat(cleanPath)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/trufflehog-3.95.7/pkg/sources/filesystem/filesystem_resume_test.go 
new/trufflehog-3.95.8/pkg/sources/filesystem/filesystem_resume_test.go
--- old/trufflehog-3.95.7/pkg/sources/filesystem/filesystem_resume_test.go      
1970-01-01 01:00:00.000000000 +0100
+++ new/trufflehog-3.95.8/pkg/sources/filesystem/filesystem_resume_test.go      
2026-07-02 20:52:07.000000000 +0200
@@ -0,0 +1,95 @@
+package filesystem
+
+import (
+       "encoding/json"
+       "os"
+       "path/filepath"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       "google.golang.org/protobuf/types/known/anypb"
+
+       trContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
+       "github.com/trufflesecurity/trufflehog/v3/pkg/sources"
+)
+
+// TestResumptionInChunksAfterRestart guards against a regression where a
+// filesystem scan restarts from the beginning after the scanner process is
+// restarted (for example by a version rollout).
+//
+// This exercises the legacy ENUMERATE_AND_SCAN path (Chunks), and it restores
+// the resume point the same way the scanner does after reclaiming an
+// in-progress job: by assigning the encoded resume info to the top-level
+// Progress.EncodedResumeInfo string, WITHOUT calling SetEncodedResumeInfoFor.
+//
+// Both of those details matter, and are why the other resumption tests in this
+// package do not catch the bug:
+//
+//   - They call ChunkUnit, which walks straight into scanDir. Chunks first 
runs
+//     s.SetProgressComplete(i, len(s.paths), "...", "") for each path, and
+//     SetProgressComplete unconditionally overwrites EncodedResumeInfo with 
its
+//     (empty) fourth argument.
+//   - They set the resume point via SetEncodedResumeInfoFor, which hydrates 
the
+//     internal encodedResumeInfoByID map. Once that map is non-nil, the empty
+//     SetProgressComplete write no longer matters, because 
GetEncodedResumeInfoFor
+//     reads the map, not the string.
+//
+// On a freshly started process the map is still nil, so the empty write 
destroys
+// the restored resume point before scanDir ever reads it, and the scan starts
+// over from the first (lowest-sorted) entry.
+func TestResumptionInChunksAfterRestart(t *testing.T) {
+       ctx := trContext.Background()
+
+       // Top-level directories that sort deterministically, mirroring the 
sorted
+       // S3-prefix layout the customer scans.
+       rootDir, err := os.MkdirTemp("", "trufflehog-resumption-chunks-test")
+       require.NoError(t, err)
+       t.Cleanup(func() { _ = os.RemoveAll(rootDir) })
+
+       dirs := []string{"aaa", "bbb", "ccc", "ddd"}
+       filesByDir := make(map[string]string, len(dirs))
+       for _, d := range dirs {
+               dirPath := filepath.Join(rootDir, d)
+               require.NoError(t, os.Mkdir(dirPath, 0755))
+               filePath := filepath.Join(dirPath, "file.txt")
+               require.NoError(t, os.WriteFile(filePath, []byte("content of 
"+d), 0644))
+               filesByDir[d] = filePath
+       }
+
+       conn, err := anypb.New(&sourcespb.Filesystem{Paths: []string{rootDir}})
+       require.NoError(t, err)
+
+       s := Source{}
+       // concurrency 1 keeps the walk deterministic and isolates this test 
from the
+       // separate out-of-order resume-write behavior of scanDir's worker pool.
+       require.NoError(t, s.Init(ctx, "test resumption chunks", 0, 0, true, 
conn, 1))
+
+       // Simulate a job restored after already scanning through bbb/file.txt.
+       // aaa and bbb must be skipped; ccc and ddd must be scanned.
+       resumePoint := filesByDir["bbb"]
+       encoded, err := json.Marshal(map[string]string{rootDir: resumePoint})
+       require.NoError(t, err)
+       s.GetProgress().EncodedResumeInfo = string(encoded)
+
+       chunksCh := make(chan *sources.Chunk, len(dirs))
+       go func() {
+               defer close(chunksCh)
+               assert.NoError(t, s.Chunks(ctx, chunksCh))
+       }()
+
+       scannedFiles := make(map[string]bool)
+       for chunk := range chunksCh {
+               scannedFiles[chunk.SourceMetadata.GetFilesystem().GetFile()] = 
true
+       }
+
+       assert.False(t, scannedFiles[filesByDir["aaa"]],
+               "aaa/file.txt should have been skipped (before resume point); 
the scan restarted from the beginning")
+       assert.False(t, scannedFiles[filesByDir["bbb"]],
+               "bbb/file.txt should have been skipped (the resume point 
itself)")
+       assert.True(t, scannedFiles[filesByDir["ccc"]],
+               "ccc/file.txt should have been scanned (after resume point)")
+       assert.True(t, scannedFiles[filesByDir["ddd"]],
+               "ddd/file.txt should have been scanned (after resume point)")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/trufflehog-3.95.7/proto/detector_type.proto 
new/trufflehog-3.95.8/proto/detector_type.proto
--- old/trufflehog-3.95.7/proto/detector_type.proto     2026-06-29 
12:17:14.000000000 +0200
+++ new/trufflehog-3.95.8/proto/detector_type.proto     2026-07-02 
20:52:07.000000000 +0200
@@ -1057,4 +1057,5 @@
   BrainTrustApiKey = 1053;
   PgAnalyzeReadKey = 1054;
   RedHatPyxis = 1055;
+  OctopusDeploy = 1056;
 }

++++++ trufflehog.obsinfo ++++++
--- /var/tmp/diff_new_pack.m322Bh/_old  2026-07-03 16:09:51.059549535 +0200
+++ /var/tmp/diff_new_pack.m322Bh/_new  2026-07-03 16:09:51.079550229 +0200
@@ -1,5 +1,5 @@
 name: trufflehog
-version: 3.95.7
-mtime: 1782728234
-commit: f446421baf832d6356c42c1743d99abff52ff334
+version: 3.95.8
+mtime: 1783018327
+commit: 00155c9dc586f34d189adc83d3ac2698c2ec551f
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/trufflehog/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.trufflehog.new.1982/vendor.tar.gz differ: char 134, 
line 1

Reply via email to