Files
trufflehog/pkg/verificationcache/verification_cache.go
Casey Tran f714bf454f
Lint / golangci-lint (push) Waiting to run
Lint / man-page-staleness (push) Waiting to run
Lint / semgrep (push) Waiting to run
Lint / checksecretparts (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Release / mark-latest (push) Canceled after 0s
Release / Release (push) Canceled after 0s
[SCAN-177] Purge secret parts from verification cache (#5318)
This PR aims to zero out secret areas of a result before caching it.
2026-09-16 11:56:14 -05:00

204 lines
6.5 KiB
Go

package verificationcache
import (
"bytes"
"encoding/binary"
"sync"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/hasher"
)
// VerificationCache is a structure that can be used to cache verification results from detectors so that a given
// credential does not trigger multiple identical remote verification attempts.
type VerificationCache struct {
metrics MetricsReporter
resultCache ResultCache
hashMu sync.Mutex
hasher hasher.Hasher
}
// New creates a new verification cache with the provided result cache and metrics reporter. If resultCache is nil, the
// verification cache will be a no-op passthrough, although it will still record relevant metrics to the provided
// metrics reporter in this case.
func New(resultCache ResultCache, metrics MetricsReporter) *VerificationCache {
if metrics == nil {
metrics = &InMemoryMetrics{}
}
return &VerificationCache{
metrics: metrics,
resultCache: resultCache,
hasher: hasher.NewBlake2B(),
}
}
// FromData is a cache-aware facade in front of the provided detector's FromData method.
//
// If the verification cache's underlying result cache is nil, or verify is false, or forceCacheUpdate is true, this
// method invokes the provided detector's FromData method with the provided arguments and returns the result. If the
// result cache is non-nil and forceCacheUpdate is true, the result cache is updated with the results before they are
// returned.
//
// Otherwise, the detector's FromData method is called with verify=false. The result cache is then checked for each
// returned result. If there is a cache hit for each result, these cached values are all returned. Otherwise, the
// detector's FromData method is called again, but with verify=true, and the results are stored in the cache and then
// returned.
//
// Detectors that implement detectors.ResultVerifier are handled by verifyCacheMisses instead, which verifies only the
// results that missed the cache rather than re-running the detector over the whole chunk.
func (v *VerificationCache) FromData(
ctx context.Context,
detector detectors.Detector,
verify bool,
forceCacheUpdate bool,
data []byte,
) ([]detectors.Result, error) {
if v.resultCache == nil {
if verify {
start := time.Now()
defer func() {
v.metrics.AddFromDataVerifyTimeSpent(time.Since(start))
}()
}
return detector.FromData(ctx, verify, data)
}
if !forceCacheUpdate {
withoutRemoteVerification, err := detector.FromData(ctx, false, data)
if err != nil {
return nil, err
}
if !verify {
return withoutRemoteVerification, nil
}
// avoiding re-running verification for every result in the chunk.
// only if a detector implements detectors.ResultVerifier
if resultVerifier, ok := detector.(detectors.ResultVerifier); ok {
return v.verifyCacheMisses(ctx, resultVerifier, withoutRemoteVerification)
}
isEverythingCached := true
var cacheHitsInCurrentChunk int
for i, r := range withoutRemoteVerification {
cacheKey, err := v.getResultCacheKey(r)
if err != nil {
ctx.Logger().Error(err, "error getting result cache key for verification caching",
"operation", "read")
isEverythingCached = false
v.metrics.AddResultCacheHitsWasted(cacheHitsInCurrentChunk)
break
}
if cacheHit, ok := v.resultCache.Get(string(cacheKey)); ok {
withoutRemoteVerification[i].CopyVerificationInfo(&cacheHit)
withoutRemoteVerification[i].VerificationFromCache = true
v.metrics.AddResultCacheHits(1)
cacheHitsInCurrentChunk++
} else {
v.metrics.AddResultCacheMisses(1)
isEverythingCached = false
v.metrics.AddResultCacheHitsWasted(cacheHitsInCurrentChunk)
break
}
}
if isEverythingCached {
v.metrics.AddCredentialVerificationsSaved(len(withoutRemoteVerification))
return withoutRemoteVerification, nil
}
}
start := time.Now()
withRemoteVerification, err := detector.FromData(ctx, verify, data)
if verify {
v.metrics.AddFromDataVerifyTimeSpent(time.Since(start))
}
if err != nil {
return nil, err
}
for _, r := range withRemoteVerification {
cacheKey, err := v.getResultCacheKey(r)
if err != nil {
ctx.Logger().Error(err, "error getting result cache key for verification caching",
"operation", "write")
continue
}
copyForCaching := r
// Do not persist raw secret values in a long-lived cache
copyForCaching.ClearSecrets()
v.resultCache.Set(string(cacheKey), copyForCaching)
}
return withRemoteVerification, nil
}
// verifyCacheMisses serves the results whose verification status is already cached and
// remotely verifies only the misses.
func (v *VerificationCache) verifyCacheMisses(
ctx context.Context,
detector detectors.ResultVerifier,
results []detectors.Result,
) ([]detectors.Result, error) {
// Only remote verification counts toward verify time; a fully cached chunk records
// nothing, matching the all-or-nothing path's early return on full cache coverage.
var timeSpentVerifying time.Duration
defer func() {
if timeSpentVerifying > 0 {
v.metrics.AddFromDataVerifyTimeSpent(timeSpentVerifying)
}
}()
verifyResult := func(i int) {
verifyStart := time.Now()
detector.VerifyResult(ctx, &results[i])
timeSpentVerifying += time.Since(verifyStart)
}
for i := range results {
cacheKey, err := v.getResultCacheKey(results[i])
if err != nil {
ctx.Logger().Error(err, "error getting result cache key for verification caching",
"operation", "read")
// Fail open: a result we cannot key still deserves verification, matching the
// all-or-nothing path, where a key error falls through to FromData(verify=true).
verifyResult(i)
continue
}
if cacheHit, ok := v.resultCache.Get(string(cacheKey)); ok {
results[i].CopyVerificationInfo(&cacheHit)
results[i].VerificationFromCache = true
v.metrics.AddResultCacheHits(1)
v.metrics.AddCredentialVerificationsSaved(1)
continue
}
v.metrics.AddResultCacheMisses(1)
verifyResult(i)
copyForCaching := results[i]
// Do not persist raw secret values in a long-lived cache
copyForCaching.ClearSecrets()
v.resultCache.Set(string(cacheKey), copyForCaching)
}
return results, nil
}
func (v *VerificationCache) getResultCacheKey(result detectors.Result) ([]byte, error) {
v.hashMu.Lock()
defer v.hashMu.Unlock()
keyBytes := bytes.Join([][]byte{result.Raw, result.RawV2}, nil)
keyBytes, err := binary.Append(keyBytes, binary.BigEndian, result.DetectorType)
if err != nil {
return nil, err
}
return v.hasher.Hash(keyBytes)
}