mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2026-06-15 13:24:37 +00:00
This PR modifies the verification cache to be detector type-aware by adding the detector type to the cache keys. There is also a slight change to the key generation logic to prevent it from reslicing the passed-in Raw value. (We haven't seen any trouble from this so far, but I think it's good to disarm the footgun while I'm here.) When adding tests, I also realized that the existing tests were inconsistently named, so I cleaned them up. This resolves #3984.
148 lines
4.4 KiB
Go
148 lines
4.4 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.
|
|
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)
|
|
}
|