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. 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 } 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.Raw = nil copyForCaching.RawV2 = nil v.resultCache.Set(string(cacheKey), copyForCaching) } return withRemoteVerification, 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) }