diff --git a/apisec/internal/config/const.go b/apisec/internal/config/const.go index 5c5f532..6483f28 100644 --- a/apisec/internal/config/const.go +++ b/apisec/internal/config/const.go @@ -5,12 +5,7 @@ package config -import "time" - const ( // MaxItemCount is the maximum amount of items to keep in a timed set. MaxItemCount = 4_096 - - // Interval is the interval between two samples being taken. - Interval = 30 * time.Second ) diff --git a/apisec/internal/timed/lru.go b/apisec/internal/timed/lru.go index b558b52..4a86f5d 100644 --- a/apisec/internal/timed/lru.go +++ b/apisec/internal/timed/lru.go @@ -7,11 +7,13 @@ package timed import ( "fmt" + "math" "math/rand" "sync/atomic" "time" "github.com/DataDog/appsec-internal-go/apisec/internal/config" + "github.com/DataDog/appsec-internal-go/log" ) // capacity is the maximum number of items that may be temporarily present in a @@ -44,18 +46,19 @@ type LRU struct { rebuilding atomic.Bool } -// NewSet initializes a new, empty [LRU] with the given interval and clock -// function. The provided interval must be at least 1 second, and may not exceed -// [config.Interval]. +// NewLRU initializes a new, empty [LRU] with the given interval and clock +// function. A warning will be logged if it is set below 1 second. Panics if +// the interval is more than [math.MaxUint32] seconds, as this value cannot be +// used internally. // // Note: timestamps are stored at second resolution, so the interval will be // rounded down to the nearest second. -func NewSet(interval time.Duration, clock ClockFunc) *LRU { +func NewLRU(interval time.Duration, clock ClockFunc) *LRU { if interval < time.Second { - panic(fmt.Errorf("NewSet: interval must be at least 1s, got %v", interval)) + log.Warn("NewLRU: interval is less than one second; this should not be attempted in production (value: %s)", interval) } - if interval > config.Interval { - panic(fmt.Errorf("NewSet: interval must not exceed %s, got %v", config.Interval, interval)) + if interval > time.Second*math.MaxUint32 { + panic(fmt.Errorf("NewLRU: interval must be <= %s, but was %s", time.Second*math.MaxUint32, interval)) } intervalSeconds := uint32(interval.Seconds()) diff --git a/apisec/internal/timed/lru_test.go b/apisec/internal/timed/lru_test.go index da6d88a..c18ad49 100644 --- a/apisec/internal/timed/lru_test.go +++ b/apisec/internal/timed/lru_test.go @@ -7,6 +7,7 @@ package timed import ( "context" + "math" "runtime" "sync" "testing" @@ -18,20 +19,19 @@ import ( ) func TestLRU(t *testing.T) { - t.Run("New", func(t *testing.T) { - require.PanicsWithError(t, "NewSet: interval must be at least 1s, got 0s", func() { NewSet(0, UnixTime) }) - require.PanicsWithError(t, "NewSet: interval must be at least 1s, got 10ms", func() { NewSet(10*time.Millisecond, UnixTime) }) - require.PanicsWithError(t, "NewSet: interval must not exceed 30s, got 1m0s", func() { NewSet(time.Minute, UnixTime) }) + t.Run("NewLRU", func(t *testing.T) { + require.PanicsWithError(t, "NewLRU: interval must be <= 1193046h28m15s, but was 1193046h28m16s", func() { NewLRU(time.Second*(math.MaxUint32+1), UnixTime) }) }) t.Run("Hit", func(t *testing.T) { fakeTime := time.Now().Unix() fakeClock := func() int64 { return fakeTime } - subject := NewSet(config.Interval, fakeClock) + const sampleIntervalSeconds = 30 + subject := NewLRU(sampleIntervalSeconds*time.Second, fakeClock) require.True(t, subject.Hit(1337)) - for range config.Interval / time.Second { + for range sampleIntervalSeconds { require.False(t, subject.Hit(1337)) fakeTime++ } @@ -63,7 +63,7 @@ func TestLRU(t *testing.T) { clock.WaitUntilDone() }() - subject := NewSet(config.Interval, clock.Unix) + subject := NewLRU(30*time.Second, clock.Unix) var ( startBarrier sync.WaitGroup diff --git a/apisec/sampler.go b/apisec/sampler.go index 2bf6365..97147e7 100644 --- a/apisec/sampler.go +++ b/apisec/sampler.go @@ -10,7 +10,6 @@ import ( "hash/fnv" "time" - "github.com/DataDog/appsec-internal-go/apisec/internal/config" "github.com/DataDog/appsec-internal-go/apisec/internal/timed" ) @@ -33,14 +32,7 @@ type ( clockFunc = func() int64 ) -// NewSampler returns a new [*Sampler] with the default clock function based on -// [time.Now]. -func NewSampler() Sampler { - return NewSamplerWithInterval(config.Interval) -} - -// NewSamplerWithInterval returns a new [*Sampler] with the specified interval -// instead of the default of 30 seconds. +// NewSamplerWithInterval returns a new [*Sampler] with the specified interval. func NewSamplerWithInterval(interval time.Duration) Sampler { return newSampler(interval, timed.UnixTime) } @@ -48,7 +40,7 @@ func NewSamplerWithInterval(interval time.Duration) Sampler { // newSampler allows creating a new [*Sampler] with custom clock function, // which is useful for testing. func newSampler(interval time.Duration, clock clockFunc) Sampler { - return (*timedSetSampler)(timed.NewSet(interval, clock)) + return (*timedSetSampler)(timed.NewLRU(interval, clock)) } // DecisionFor makes a sampling decision for the provided [SamplingKey]. If it diff --git a/appsec/config.go b/appsec/config.go index 2f0ac56..f24f0c3 100644 --- a/appsec/config.go +++ b/appsec/config.go @@ -36,12 +36,18 @@ const ( EnvRules = "DD_APPSEC_RULES" // EnvRASPEnabled is the env var used to enable/disable RASP functionalities for ASM EnvRASPEnabled = "DD_APPSEC_RASP_ENABLED" + + // envAPISecSampleDelay is the env var used to set the delay for the API Security sampler in system tests. + // It is not indended to be set by users. + envAPISecSampleDelay = "DD_API_SECURITY_SAMPLE_DELAY" ) // Configuration constants and default values const ( // DefaultAPISecSampleRate is the default rate at which API Security schemas are extracted from requests DefaultAPISecSampleRate = .1 + // DefaultAPISecSampleInterval is the default interval between two samples being taken. + DefaultAPISecSampleInterval = 30 * time.Second // DefaultObfuscatorKeyRegex is the default regexp used to obfuscate keys DefaultObfuscatorKeyRegex = `(?i)pass|pw(?:or)?d|secret|(?:api|private|public|access)[_-]?key|token|consumer[_-]?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization|jsessionid|phpsessid|asp\.net[_-]sessionid|sid|jwt` // DefaultObfuscatorValueRegex is the default regexp used to obfuscate values @@ -73,7 +79,7 @@ type APISecOption func(*APISecConfig) func NewAPISecConfig(opts ...APISecOption) APISecConfig { cfg := APISecConfig{ Enabled: boolEnv(EnvAPISecEnabled, true), - Sampler: apisec.NewSampler(), + Sampler: apisec.NewSamplerWithInterval(durationEnv(envAPISecSampleDelay, "s", DefaultAPISecSampleInterval)), SampleRate: readAPISecuritySampleRate(), } for _, opt := range opts { @@ -224,3 +230,16 @@ func boolEnv(key string, def bool) bool { } return v } + +func durationEnv(key string, unit string, def time.Duration) time.Duration { + strVal, ok := os.LookupEnv(key) + if !ok { + return def + } + val, err := time.ParseDuration(strVal + unit) + if err != nil { + logEnvVarParsingError(key, strVal, err, def) + return def + } + return val +} diff --git a/appsec/config_test.go b/appsec/config_test.go index 76490eb..c6cc762 100644 --- a/appsec/config_test.go +++ b/appsec/config_test.go @@ -238,3 +238,37 @@ func TestRASPEnablement(t *testing.T) { require.True(t, RASPEnabled()) }) } + +func TestDurationEnv(t *testing.T) { + const varName = "DD_TEST_VARIABLE_DURATION" + + type testCase struct { + EnvVal string + EnvUnit string + Expected time.Duration + } + testCases := map[string]testCase{ + "blank": { + EnvVal: "", + EnvUnit: "s", + Expected: 1337, + }, + "1m": { + EnvVal: "1", + EnvUnit: "m", + Expected: time.Minute, + }, + "invalid": { + EnvVal: "invalid", + EnvUnit: "s", + Expected: 1337, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Setenv(varName, tc.EnvVal) + require.Equal(t, tc.Expected, durationEnv(varName, tc.EnvUnit, 1337)) + }) + } +}