diff --git a/pkg/serverless/appsec/appsec.go b/pkg/serverless/appsec/appsec.go index e7df51435ee8b9..36888875127388 100644 --- a/pkg/serverless/appsec/appsec.go +++ b/pkg/serverless/appsec/appsec.go @@ -9,6 +9,7 @@ package appsec import ( "encoding/json" + "math/rand" "time" "github.com/DataDog/appsec-internal-go/appsec" @@ -117,10 +118,10 @@ func (a *AppSec) Monitor(addresses map[string]any) (res waf.Result) { defer ctx.Close() timeout := a.cfg.WafTimeout - // Naive 10% sampling for API Security, disabled for testing - // if rand.Uint32()%10 == 0 { - addresses["waf.context.processor"] = map[string]any{"extract-schema": true} - // } + // Ask the WAF for schema reporting if API security is enabled + if config.APISecurityEnabled() && config.APISecuritySampleRate() >= rand.Float64() { + addresses["waf.context.processor"] = map[string]any{"extract-schema": true} + } res, err := ctx.Run(waf.RunAddressData{Persistent: addresses}, timeout) if err != nil { diff --git a/pkg/serverless/appsec/appsec_test.go b/pkg/serverless/appsec/appsec_test.go index 49d691b8d57f43..39434b5f1b7f4b 100644 --- a/pkg/serverless/appsec/appsec_test.go +++ b/pkg/serverless/appsec/appsec_test.go @@ -11,6 +11,7 @@ import ( "strconv" "testing" + "github.com/DataDog/datadog-agent/pkg/serverless/appsec/config" waf "github.com/DataDog/go-libddwaf" "github.com/stretchr/testify/require" @@ -74,6 +75,9 @@ func TestMonitor(t *testing.T) { }) t.Run("api-security", func(t *testing.T) { + t.Setenv("DD_API_SECURITY_REQUEST_SAMPLE_RATE", "1.0") + t.Setenv("DD_EXPERIMENTAL_API_SECURITY_ENABLED", "true") + config.Refresh() for _, tc := range []struct { name string pathParams map[string]any diff --git a/pkg/serverless/appsec/config/config.go b/pkg/serverless/appsec/config/config.go index b298f295b94910..e02916aa69594d 100644 --- a/pkg/serverless/appsec/config/config.go +++ b/pkg/serverless/appsec/config/config.go @@ -20,16 +20,19 @@ import ( ) const ( - enabledEnvVar = "DD_SERVERLESS_APPSEC_ENABLED" - rulesEnvVar = "DD_APPSEC_RULES" - wafTimeoutEnvVar = "DD_APPSEC_WAF_TIMEOUT" - traceRateLimitEnvVar = "DD_APPSEC_TRACE_RATE_LIMIT" - obfuscatorKeyEnvVar = "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP" - obfuscatorValueEnvVar = "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP" - tracingEnabledEnvVar = "DD_APM_TRACING_ENABLED" + enabledEnvVar = "DD_SERVERLESS_APPSEC_ENABLED" + rulesEnvVar = "DD_APPSEC_RULES" + wafTimeoutEnvVar = "DD_APPSEC_WAF_TIMEOUT" + traceRateLimitEnvVar = "DD_APPSEC_TRACE_RATE_LIMIT" + obfuscatorKeyEnvVar = "DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP" + obfuscatorValueEnvVar = "DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP" + tracingEnabledEnvVar = "DD_APM_TRACING_ENABLED" + apiSecurityEnabledEnvVar = "DD_EXPERIMENTAL_API_SECURITY_ENABLED" + apiSecuritySampleRateEnvVar = "DD_API_SECURITY_REQUEST_SAMPLE_RATE" ) const ( + defaultAPISecSampleRate = 10. / 100 defaultWAFTimeout = 4 * time.Millisecond defaultTraceRate = 100 // up to 100 appsec traces/s defaultObfuscatorKeyRegex = `(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?)key)|token|consumer_?(?:id|key|secret)|sign(?:ed|ature)|bearer|authorization` @@ -57,8 +60,19 @@ type ObfuscatorConfig struct { ValueRegex string } -// standalone is assigned at init by reading the environment -var standalone bool +// Config variables assigned at init by reading the environment +var ( + apiSecEnabled bool + apiSecSampleRate float64 + standalone bool +) + +// Refresh updates the configuration by reading from the environment +func Refresh() { + standalone = isStandalone() + apiSecEnabled = apiSecurityEnabled() + apiSecSampleRate = apiSecuritySampleRate() +} // IsEnabled returns true when appsec is enabled when the environment variable // It also returns whether the env var is actually set in the env or not @@ -79,6 +93,16 @@ func IsStandalone() bool { return standalone } +// APISecurityEnabled returns whether API security is enabled through the environment +func APISecurityEnabled() bool { + return apiSecEnabled +} + +// APISecuritySampleRate returns the sample rate for API Security schema collection, read from the env +func APISecuritySampleRate() float64 { + return apiSecSampleRate +} + // NewConfig returns a new appsec configuration read from the environment func NewConfig() (*Config, error) { rules, err := readRulesConfig() @@ -191,6 +215,24 @@ func isStandalone() bool { return set && !enabled } +func apiSecurityEnabled() bool { + enabled, _ := strconv.ParseBool(os.Getenv(apiSecurityEnabledEnvVar)) + return enabled +} + +func apiSecuritySampleRate() float64 { + rate, err := strconv.ParseFloat(os.Getenv(apiSecuritySampleRateEnvVar), 64) + if err != nil { + log.Debugf("appsec: could not parse %s. Defaulting to %f", apiSecuritySampleRateEnvVar, defaultAPISecSampleRate) + return defaultAPISecSampleRate + } + if rate < 0. || rate > 1. { + log.Debugf("appsec: %s value must be between 0 and 1. Defaulting to %f", apiSecuritySampleRateEnvVar, defaultAPISecSampleRate) + return defaultAPISecSampleRate + } + return rate +} + func init() { - standalone = isStandalone() + Refresh() } diff --git a/pkg/serverless/appsec/config/config_test.go b/pkg/serverless/appsec/config/config_test.go index 38ec57d1295862..1e27be214f50c5 100644 --- a/pkg/serverless/appsec/config/config_test.go +++ b/pkg/serverless/appsec/config/config_test.go @@ -287,12 +287,70 @@ func TestConfig(t *testing.T) { if tc.env != "" { require.NoError(t, os.Setenv(tracingEnabledEnvVar, tc.env)) } - standalone = isStandalone() + Refresh() require.Equal(t, tc.standalone, IsStandalone()) }) } }) + + t.Run("api security", func(t *testing.T) { + for _, tc := range []struct { + name string + enabledVar string + sampleRateVar string + enabled bool + sampleRate float64 + }{ + { + name: "disabled", + sampleRate: defaultAPISecSampleRate, + }, + { + name: "disabled", + enabledVar: "false", + sampleRate: defaultAPISecSampleRate, + }, + { + name: "disabled", + enabledVar: "0", + sampleRate: defaultAPISecSampleRate, + }, + { + name: "disabled", + enabledVar: "weirdvalue", + sampleRate: defaultAPISecSampleRate, + }, + { + name: "enabled", + enabledVar: "true", + enabled: true, + sampleRate: defaultAPISecSampleRate, + }, + { + name: "enabled", + enabledVar: "1", + enabled: true, + sampleRate: defaultAPISecSampleRate, + }, + { + name: "sampleRate 1.0", + enabledVar: "true", + sampleRateVar: "1.0", + enabled: true, + sampleRate: 1.0, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(apiSecurityEnabledEnvVar, tc.enabledVar) + t.Setenv(apiSecuritySampleRateEnvVar, tc.sampleRateVar) + Refresh() + require.Equal(t, tc.enabled, APISecurityEnabled()) + require.Equal(t, tc.sampleRate, APISecuritySampleRate()) + }) + } + + }) } func cleanEnv() func() {