engine: route OAuth2-authenticated verification through OAuthVerifier
Add VerifierAuth to engine.Config so callers can pass pre-built token sources keyed by detector name. During engine init, the custom verifier filter attaches each token source to its detector's EndpointSetter via SetOAuth2TokenSource. In detectChunk, when a detector satisfies OAuthVerifier and has a token source configured, verification bypasses the built-in path entirely: the detector runs with verify=false to extract candidates, then each result is verified via OAuth2-authenticated POST through OAuthVerify in oauth_verifier.go. OAuthVerify builds an authenticated HTTP client, POSTs the result using a customer-defined request body template with $token references, and interprets the HTTP status code against successRanges/rotatedRanges. All log messages inherit the oauth2_trace correlation ID from the token source via TracedTokenSource.EnrichContext. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+62
-10
@@ -110,6 +110,16 @@ type Config struct {
|
||||
ExcludeDetectors string
|
||||
CustomVerifiersOnly bool
|
||||
VerifierEndpoints map[string]string
|
||||
// VerifierAuth holds OAuth2 token sources for custom verifiers,
|
||||
// keyed by detector name (matching VerifierEndpoints keys). When
|
||||
// a detector has a matching entry here, its token source is set
|
||||
// on the detector's EndpointSetter so the engine can perform
|
||||
// OAuth2-authenticated verification via the OAuthVerifier interface.
|
||||
VerifierAuth map[config.DetectorID]detectors.OAuth2TokenSource
|
||||
|
||||
// NoIgnoreTag disables the "trufflehog:ignore" tag. If set to true, results are
|
||||
// reported even when the line they were found on carries the tag.
|
||||
NoIgnoreTag bool
|
||||
|
||||
// Verify determines whether the scanner will verify candidate secrets.
|
||||
Verify bool
|
||||
@@ -126,10 +136,6 @@ type Config struct {
|
||||
FilterUnverified bool
|
||||
ShouldScanEntireChunk bool
|
||||
|
||||
// NoIgnoreTag disables the "trufflehog:ignore" tag. If set to true, results are
|
||||
// reported even when the line they were found on carries the tag.
|
||||
NoIgnoreTag bool
|
||||
|
||||
Dispatcher ResultsDispatcher
|
||||
|
||||
// SourceManager orchestrates source enumeration and concurrent chunk production.
|
||||
@@ -326,6 +332,13 @@ func NewEngine(ctx context.Context, cfg *Config) (*Engine, error) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the custom verifier has OAuth2 auth configured, attach
|
||||
// the token source to the detector. The scan loop checks
|
||||
// OAuthVerifier to route verification through OAuth2.
|
||||
if ts, hasAuth := getWithDetectorID(d, cfg.VerifierAuth); hasAuth {
|
||||
customizer.SetOAuth2TokenSource(ts)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -1170,12 +1183,51 @@ func (e *Engine) detectChunk(ctx context.Context, data detectableChunk) {
|
||||
t := time.AfterFunc(detectionTimeout+1*time.Second, func() {
|
||||
ctx.Logger().Error(nil, "a detector ignored the context timeout")
|
||||
})
|
||||
results, err := e.verificationCache.FromData(
|
||||
ctx,
|
||||
data.detector.Detector,
|
||||
data.verify,
|
||||
data.chunk.SecretID != 0,
|
||||
matchBytes)
|
||||
|
||||
// When the detector has OAuth2 auth on its custom verifier,
|
||||
// bypass the built-in verification path entirely: detect only,
|
||||
// then verify each result via OAuth2-authenticated POST. This
|
||||
// intentionally skips verificationCache — the built-in cache
|
||||
// wraps FromData with verify=true, which would run the
|
||||
// detector's unauthenticated verification. The OAuth path
|
||||
// replaces that with its own authenticated verification loop.
|
||||
// Token caching is handled separately by oauth2.ReuseTokenSource.
|
||||
var results []detectors.Result
|
||||
var err error
|
||||
if oauthV, ok := data.detector.Detector.(detectors.OAuthVerifier); ok && oauthV.HasOAuth2() {
|
||||
// Extract the trace from the token source and set it on
|
||||
// the context once. All downstream logs inherit it.
|
||||
oauthCtx := ctx
|
||||
if ts, ok := oauthV.OAuth2TokenSource().(*detectors.TracedTokenSource); ok {
|
||||
oauthCtx = ts.EnrichContext(ctx)
|
||||
}
|
||||
oauthCtx.Logger().Info("using OAuth2 verification path")
|
||||
results, err = data.detector.FromData(ctx, false, matchBytes)
|
||||
if err == nil && data.verify && len(results) > 0 {
|
||||
// Build VerifyEndpoints from the detector's endpoint
|
||||
// list. Callers can populate headers, ranges, and
|
||||
// body templates on the config; until then, endpoints
|
||||
// use the default (200 = verified).
|
||||
configs := make([]OAuthVerifyConfig, len(oauthV.Endpoints()))
|
||||
for i, ep := range oauthV.Endpoints() {
|
||||
configs[i] = OAuthVerifyConfig{Endpoint: ep}
|
||||
}
|
||||
for i := range results {
|
||||
verified, verifyErr := OAuthVerify(
|
||||
oauthCtx, nil, oauthV.OAuth2TokenSource(), configs, &results[i])
|
||||
results[i].Verified = verified
|
||||
results[i].SetVerificationError(verifyErr, string(results[i].Raw))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
results, err = e.verificationCache.FromData(
|
||||
ctx,
|
||||
data.detector.Detector,
|
||||
data.verify,
|
||||
data.chunk.SecretID != 0,
|
||||
matchBytes)
|
||||
}
|
||||
|
||||
t.Stop()
|
||||
cancel()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// oauth_verifier.go adapts the engine's per-detector OAuth2 config into
|
||||
// the shared VerifyCredential loop. The engine scan loop calls OAuthVerify
|
||||
// when a detector has a custom verifier with OAuth2 auth configured.
|
||||
//
|
||||
// All core verification logic (body template resolution, header
|
||||
// application, OAuth client wrapping, status code interpretation,
|
||||
// endpoint fallback, response capture, outcome logging) lives in the
|
||||
// shared VerifyCredential function in pkg/custom_detectors/verify.go.
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
custom_detectors "github.com/trufflesecurity/trufflehog/v3/pkg/custom_detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
)
|
||||
|
||||
var defaultOAuthBaseClient = common.SaneHttpClient()
|
||||
|
||||
// OAuthVerifyConfig holds the per-endpoint settings the engine needs
|
||||
// to perform OAuth2-authenticated verification. It mirrors the
|
||||
// relevant fields from VerifierConfig without importing the proto
|
||||
// directly into the detectors package.
|
||||
type OAuthVerifyConfig struct {
|
||||
Endpoint string
|
||||
SuccessRanges []string
|
||||
RotatedRanges []string
|
||||
// RequestBody is the customer-defined body template with $token
|
||||
// references. Nil means use a default body with just the secret.
|
||||
RequestBody map[string]string
|
||||
Headers []string
|
||||
}
|
||||
|
||||
// OAuthVerify converts the engine's OAuthVerifyConfig list into the
|
||||
// shared VerifyEndpoint format and delegates to VerifyCredential.
|
||||
// The caller is expected to set "oauth2_trace" on the context's logger
|
||||
// before calling so all log messages carry the correlation ID.
|
||||
func OAuthVerify(
|
||||
ctx context.Context,
|
||||
baseClient *http.Client,
|
||||
ts detectors.OAuth2TokenSource,
|
||||
configs []OAuthVerifyConfig,
|
||||
result *detectors.Result,
|
||||
) (bool, error) {
|
||||
logger := logContext.AddLogger(ctx).Logger()
|
||||
if len(configs) == 0 {
|
||||
logger.Error(nil, "no verification endpoints configured",
|
||||
"detector_type", result.DetectorType.String(),
|
||||
"detector_name", result.DetectorName,
|
||||
)
|
||||
return false, fmt.Errorf("no verification endpoints configured")
|
||||
}
|
||||
|
||||
if baseClient == nil {
|
||||
baseClient = defaultOAuthBaseClient
|
||||
}
|
||||
|
||||
// Convert engine configs to the shared VerifyEndpoint format.
|
||||
// All endpoints share the same token source in the engine path.
|
||||
endpoints := make([]custom_detectors.VerifyEndpoint, len(configs))
|
||||
for i, cfg := range configs {
|
||||
endpoints[i] = custom_detectors.VerifyEndpoint{
|
||||
URL: cfg.Endpoint,
|
||||
Headers: cfg.Headers,
|
||||
SuccessRanges: cfg.SuccessRanges,
|
||||
RotatedRanges: cfg.RotatedRanges,
|
||||
Body: cfg.RequestBody,
|
||||
TokenSource: ts,
|
||||
}
|
||||
}
|
||||
|
||||
vars := map[string]string{
|
||||
"$secret": string(result.Raw),
|
||||
"$detector_type": result.DetectorType.String(),
|
||||
"$detector_name": result.DetectorName,
|
||||
}
|
||||
|
||||
outcome := custom_detectors.VerifyCredential(ctx, baseClient, endpoints, result.Raw, vars)
|
||||
|
||||
if !outcome.Definitive && outcome.RangesInEffect {
|
||||
return false, fmt.Errorf("all verification endpoints exhausted; no status code matched configured ranges")
|
||||
}
|
||||
return outcome.Verified, nil
|
||||
}
|
||||
Reference in New Issue
Block a user