Skip to content
This repository was archived by the owner on Apr 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions apisec/internal/config/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
17 changes: 10 additions & 7 deletions apisec/internal/timed/lru.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
14 changes: 7 additions & 7 deletions apisec/internal/timed/lru_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package timed

import (
"context"
"math"
"runtime"
"sync"
"testing"
Expand All @@ -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++
}
Expand Down Expand Up @@ -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
Expand Down
12 changes: 2 additions & 10 deletions apisec/sampler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -33,22 +32,15 @@ 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)
}

// 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
Expand Down
21 changes: 20 additions & 1 deletion appsec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
34 changes: 34 additions & 0 deletions appsec/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
Loading