Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (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
This PR introduces a cache that allows the scanner to avoid emitting multiple requests to verify the same credential. In practice, it doesn't seem to reduce scan time at all, but it does seem to reduce the number of calls to FromData rather drastically. The cache is implemented as an opt-out feature that can be disabled with a new CLI flag. If we don't like this, we can change it. The metrics collection hopefully isn't too architecture-astronauty; I wanted to create something useful here that could also accommodate future Prometheus configuration without making the implementation all stupid.
241 lines
9.1 KiB
Go
241 lines
9.1 KiB
Go
package verificationcache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/cache/simple"
|
|
logContext "github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
|
)
|
|
|
|
type testDetector struct {
|
|
fromDataCallCount int
|
|
results []detectors.Result
|
|
}
|
|
|
|
func (t *testDetector) FromData(_ context.Context, verify bool, _ []byte) ([]detectors.Result, error) {
|
|
t.fromDataCallCount = t.fromDataCallCount + 1
|
|
var results []detectors.Result
|
|
for _, r := range t.results {
|
|
copy := detectors.Result{Redacted: r.Redacted, Raw: r.Raw, RawV2: r.RawV2}
|
|
if verify {
|
|
copy.CopyVerificationInfo(&r)
|
|
}
|
|
results = append(results, copy)
|
|
}
|
|
|
|
// The metric timing resolution is 1 ms, so the detector needs to artificially slow down so that it can actually be
|
|
// monitored.
|
|
time.Sleep(2 * time.Millisecond)
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (t *testDetector) Keywords() []string { return nil }
|
|
func (t *testDetector) Type() detectorspb.DetectorType { return -1 }
|
|
func (t *testDetector) Description() string { return "" }
|
|
|
|
var _ detectors.Detector = (*testDetector)(nil)
|
|
|
|
func getResultCacheKey(t *testing.T, cache *VerificationCache, result detectors.Result) string {
|
|
key, err := cache.getResultCacheKey(result)
|
|
require.NoError(t, err)
|
|
return string(key)
|
|
}
|
|
|
|
func TestVerificationCacheFromData_Passthrough(t *testing.T) {
|
|
detector := testDetector{results: []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: true},
|
|
}}
|
|
|
|
require.NotPanics(t, func() {
|
|
cache := New(nil, nil)
|
|
results, err := cache.FromData(
|
|
logContext.Background(),
|
|
&detector,
|
|
true,
|
|
true,
|
|
nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, detector.fromDataCallCount)
|
|
assert.ElementsMatch(t, detector.results, results)
|
|
})
|
|
}
|
|
|
|
func TestVerificationCacheFromData_VerifyFalseForceCacheUpdateFalse(t *testing.T) {
|
|
detector := testDetector{results: []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: true},
|
|
}}
|
|
metrics := InMemoryMetrics{}
|
|
cache := New(simple.NewCache[detectors.Result](), &metrics)
|
|
|
|
results, err := cache.FromData(
|
|
logContext.Background(),
|
|
&detector,
|
|
false,
|
|
false,
|
|
nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, detector.fromDataCallCount)
|
|
assert.ElementsMatch(t, []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: false},
|
|
}, results)
|
|
assert.Empty(t, cache.resultCache.Values())
|
|
assert.Equal(t, int64(0), metrics.FromDataVerifyTimeSpentMS.Load())
|
|
assert.Equal(t, int32(0), metrics.CredentialVerificationsSaved.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheHits.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheHitsWasted.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheMisses.Load())
|
|
}
|
|
|
|
func TestFromDataCached_VerifyFalseForceCacheUpdateTrue(t *testing.T) {
|
|
detector := testDetector{results: []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: true},
|
|
{Redacted: "world", Raw: []byte("world"), RawV2: []byte("worldV2"), Verified: false},
|
|
}}
|
|
detector.results[1].SetVerificationError(errors.New("test verification error"))
|
|
metrics := InMemoryMetrics{}
|
|
cache := New(simple.NewCache[detectors.Result](), &metrics)
|
|
|
|
results, err := cache.FromData(
|
|
logContext.Background(),
|
|
&detector,
|
|
false,
|
|
true,
|
|
nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, detector.fromDataCallCount)
|
|
assert.ElementsMatch(t, []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: false},
|
|
{Redacted: "world", Raw: []byte("world"), RawV2: []byte("worldV2"), Verified: false},
|
|
}, results)
|
|
assert.ElementsMatch(t, []detectors.Result{
|
|
{Redacted: "hello", Verified: false},
|
|
{Redacted: "world", Verified: false},
|
|
}, cache.resultCache.Values())
|
|
assert.Equal(t, int64(0), metrics.FromDataVerifyTimeSpentMS.Load())
|
|
assert.Equal(t, int32(0), metrics.CredentialVerificationsSaved.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheHits.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheHitsWasted.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheMisses.Load())
|
|
}
|
|
|
|
func TestFromDataCached_VerifyTrueForceCacheUpdateFalseAllCacheHits(t *testing.T) {
|
|
remoteResults := []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: true},
|
|
{Redacted: "world", Raw: []byte("world"), RawV2: []byte("worldV2"), Verified: false},
|
|
}
|
|
remoteResults[1].SetVerificationError(errors.New("test verification error"))
|
|
detector := testDetector{results: remoteResults}
|
|
cacheData := []detectors.Result{
|
|
{Redacted: "hello", Verified: false},
|
|
{Redacted: "world", Verified: true},
|
|
}
|
|
cacheData[0].SetVerificationError(errors.New("test verification error"))
|
|
metrics := InMemoryMetrics{}
|
|
cache := New(simple.NewCache[detectors.Result](), &metrics)
|
|
cache.resultCache.Set(getResultCacheKey(t, cache, remoteResults[0]), cacheData[0])
|
|
cache.resultCache.Set(getResultCacheKey(t, cache, remoteResults[1]), cacheData[1])
|
|
|
|
results, err := cache.FromData(
|
|
logContext.Background(),
|
|
&detector,
|
|
true,
|
|
false,
|
|
nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, detector.fromDataCallCount)
|
|
wantResults := []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: false, VerificationFromCache: true},
|
|
{Redacted: "world", Raw: []byte("world"), RawV2: []byte("worldV2"), Verified: true, VerificationFromCache: true},
|
|
}
|
|
wantResults[0].SetVerificationError(errors.New("test verification error"))
|
|
assert.ElementsMatch(t, wantResults, results)
|
|
assert.ElementsMatch(t, cacheData, cache.resultCache.Values())
|
|
assert.Equal(t, int64(0), metrics.FromDataVerifyTimeSpentMS.Load())
|
|
assert.Equal(t, int32(2), metrics.CredentialVerificationsSaved.Load())
|
|
assert.Equal(t, int32(2), metrics.ResultCacheHits.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheHitsWasted.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheMisses.Load())
|
|
}
|
|
|
|
func TestFromDataCached_VerifyTrueForceCacheUpdateFalseCacheMiss(t *testing.T) {
|
|
detector := testDetector{results: []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: true},
|
|
{Redacted: "world", Raw: []byte("world"), RawV2: []byte("worldV2"), Verified: false},
|
|
}}
|
|
detector.results[1].SetVerificationError(errors.New("test verification error"))
|
|
cachedResult := detectors.Result{Redacted: "hello", Verified: false}
|
|
cachedResult.SetVerificationError(errors.New("test verification error"))
|
|
resultCache := simple.NewCache[detectors.Result]()
|
|
metrics := InMemoryMetrics{}
|
|
cache := New(resultCache, &metrics)
|
|
cache.resultCache.Set(getResultCacheKey(t, cache, detector.results[0]), cachedResult)
|
|
|
|
results, err := cache.FromData(
|
|
logContext.Background(),
|
|
&detector,
|
|
true,
|
|
false,
|
|
nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, detector.fromDataCallCount)
|
|
assert.ElementsMatch(t, detector.results, results)
|
|
wantCacheData := []detectors.Result{
|
|
{Redacted: "hello", Verified: true},
|
|
{Redacted: "world", Verified: false},
|
|
}
|
|
wantCacheData[1].SetVerificationError(errors.New("test verification error"))
|
|
assert.ElementsMatch(t, wantCacheData, cache.resultCache.Values())
|
|
assert.Less(t, int64(0), metrics.FromDataVerifyTimeSpentMS.Load())
|
|
assert.Equal(t, int32(0), metrics.CredentialVerificationsSaved.Load())
|
|
assert.Equal(t, int32(1), metrics.ResultCacheHits.Load())
|
|
assert.Equal(t, int32(1), metrics.ResultCacheMisses.Load())
|
|
assert.Equal(t, int32(1), metrics.ResultCacheHitsWasted.Load())
|
|
}
|
|
|
|
func TestFromDataCached_VerifyTrueForceCacheUpdateTrue(t *testing.T) {
|
|
detector := testDetector{results: []detectors.Result{
|
|
{Redacted: "hello", Raw: []byte("hello"), RawV2: []byte("helloV2"), Verified: true},
|
|
{Redacted: "world", Raw: []byte("world"), RawV2: []byte("worldV2"), Verified: false},
|
|
}}
|
|
detector.results[1].SetVerificationError(errors.New("test verification error"))
|
|
metrics := InMemoryMetrics{}
|
|
cache := New(simple.NewCache[detectors.Result](), &metrics)
|
|
cache.resultCache.Set(getResultCacheKey(t, cache, detector.results[0]), detectors.Result{Redacted: "hello", Verified: false})
|
|
cache.resultCache.Set(getResultCacheKey(t, cache, detector.results[1]), detectors.Result{Redacted: "world", Verified: true})
|
|
|
|
results, err := cache.FromData(
|
|
logContext.Background(),
|
|
&detector,
|
|
true,
|
|
true,
|
|
nil)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, detector.fromDataCallCount)
|
|
assert.ElementsMatch(t, detector.results, results)
|
|
wantCacheData := []detectors.Result{
|
|
{Redacted: "hello", Verified: true},
|
|
{Redacted: "world", Verified: false},
|
|
}
|
|
wantCacheData[1].SetVerificationError(errors.New("test verification error"))
|
|
assert.ElementsMatch(t, wantCacheData, cache.resultCache.Values())
|
|
assert.Less(t, int64(0), metrics.FromDataVerifyTimeSpentMS.Load())
|
|
assert.Equal(t, int32(0), metrics.CredentialVerificationsSaved.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheHits.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheMisses.Load())
|
|
assert.Equal(t, int32(0), metrics.ResultCacheHitsWasted.Load())
|
|
}
|