diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index dbdfe8135..709faae49 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -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 { diff --git a/pkg/engine/oauth_verifier.go b/pkg/engine/oauth_verifier.go new file mode 100644 index 000000000..fe45fd246 --- /dev/null +++ b/pkg/engine/oauth_verifier.go @@ -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 +}