Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: WAF truncation telemetry #2605

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions go.mod
Expand Up @@ -9,7 +9,7 @@ require (
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1
github.com/DataDog/datadog-go/v5 v5.3.0
github.com/DataDog/go-libddwaf/v2 v2.3.2
github.com/DataDog/go-libddwaf/v2 v2.3.3-0.20240228092219-fbf1bf122331
github.com/DataDog/gostackparse v0.7.0
github.com/DataDog/sketches-go v1.4.2
github.com/IBM/sarama v1.40.0
Expand Down Expand Up @@ -92,7 +92,7 @@ require (
go.uber.org/atomic v1.11.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.9.0
golang.org/x/sys v0.16.0
golang.org/x/sys v0.17.0
golang.org/x/time v0.3.0
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
google.golang.org/api v0.128.0
Expand Down Expand Up @@ -146,7 +146,7 @@ require (
github.com/eapache/go-resiliency v1.4.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/ebitengine/purego v0.6.0-alpha.5 // indirect
github.com/ebitengine/purego v0.6.1 // indirect
github.com/elastic/elastic-transport-go/v8 v8.1.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Expand Up @@ -633,8 +633,8 @@ github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1/go.mod h1:Vc+snp
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
github.com/DataDog/go-libddwaf/v2 v2.3.2 h1:pdi9xjWW57IpOpTeOyPuNveEDFLmmInsHDeuZk3TY34=
github.com/DataDog/go-libddwaf/v2 v2.3.2/go.mod h1:gsCdoijYQfj8ce/T2bEDNPZFIYnmHluAgVDpuQOWMZE=
github.com/DataDog/go-libddwaf/v2 v2.3.3-0.20240228092219-fbf1bf122331 h1:tzonLL0IrzRLuf1b7F8puRlMISBPHJ/ot7n2Sl732vI=
github.com/DataDog/go-libddwaf/v2 v2.3.3-0.20240228092219-fbf1bf122331/go.mod h1:gsCdoijYQfj8ce/T2bEDNPZFIYnmHluAgVDpuQOWMZE=
github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I=
github.com/DataDog/go-tuf v1.0.2-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
Expand Down Expand Up @@ -1062,8 +1062,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.6.0-alpha.5 h1:EYID3JOAdmQ4SNZYJHu9V6IqOeRQDBYxqKAg9PyoHFY=
github.com/ebitengine/purego v0.6.0-alpha.5/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/ebitengine/purego v0.6.1 h1:sjN8rfzbhXQ59/pE+wInswbU9aMDHiwlup4p/a07Mkg=
github.com/ebitengine/purego v0.6.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elastic/elastic-transport-go/v8 v8.1.0 h1:NeqEz1ty4RQz+TVbUrpSU7pZ48XkzGWQj02k5koahIE=
github.com/elastic/elastic-transport-go/v8 v8.1.0/go.mod h1:87Tcz8IVNe6rVSLdBux1o/PEItLtyabHU3naC7IoqKI=
github.com/elastic/go-elasticsearch/v6 v6.8.5 h1:U2HtkBseC1FNBmDr0TR2tKltL6FxoY+niDAlj5M8TK8=
Expand Down Expand Up @@ -2534,8 +2534,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
1 change: 1 addition & 0 deletions internal/appsec/listener/graphqlsec/graphql.go
Expand Up @@ -96,6 +96,7 @@ func (l *wafEventListener) onEvent(request *types.RequestOperation, _ types.Requ
},
},
l.config.WAFTimeout,
query.TagSetter,
)
shared.AddSecurityEvents(field, l.limiter, wafResult.Events)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/appsec/listener/grpcsec/grpc.go
Expand Up @@ -112,7 +112,7 @@ func (l *wafEventListener) onEvent(op *types.HandlerOperation, handlerArgs types
values := map[string]any{
UserIDAddr: args.UserID,
}
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout)
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout, &op.TagsHolder)
if wafResult.HasActions() || wafResult.HasEvents() {
for _, id := range wafResult.Actions {
if a, ok := l.actions[id]; ok && a.Blocking() {
Expand All @@ -135,7 +135,7 @@ func (l *wafEventListener) onEvent(op *types.HandlerOperation, handlerArgs types
values[HTTPClientIPAddr] = handlerArgs.ClientIP.String()
}

wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout)
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout, &op.TagsHolder)
if wafResult.HasActions() || wafResult.HasEvents() {
interrupt := shared.ProcessActions(op, l.actions, wafResult.Actions)
shared.AddSecurityEvents(op, l.limiter, wafResult.Events)
Expand Down Expand Up @@ -172,7 +172,7 @@ func (l *wafEventListener) onEvent(op *types.HandlerOperation, handlerArgs types

// Run the WAF, ignoring the returned actions - if any - since blocking after the request handler's
// response is not supported at the moment.
wafResult := shared.RunWAF(wafCtx, values, l.config.WAFTimeout)
wafResult := shared.RunWAF(wafCtx, values, l.config.WAFTimeout, &op.TagsHolder)

if wafResult.HasEvents() {
log.Debug("appsec: attack detected by the grpc waf")
Expand Down
8 changes: 4 additions & 4 deletions internal/appsec/listener/httpsec/http.go
Expand Up @@ -106,7 +106,7 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat
// see if the associated user should be blocked. Since we don't control the execution flow in this case
// (SetUser is SDK), we delegate the responsibility of interrupting the handler to the user.
dyngo.On(op, func(operation *sharedsec.UserIDOperation, args sharedsec.UserIDOperationArgs) {
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: map[string]any{UserIDAddr: args.UserID}}, l.config.WAFTimeout)
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: map[string]any{UserIDAddr: args.UserID}}, l.config.WAFTimeout, &op.TagsHolder)
if wafResult.HasActions() || wafResult.HasEvents() {
processHTTPSDKAction(operation, l.actions, wafResult.Actions)
shared.AddSecurityEvents(op, l.limiter, wafResult.Events)
Expand Down Expand Up @@ -150,7 +150,7 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat
values["waf.context.processor"] = map[string]any{"extract-schema": true}
}

wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout)
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout, &op.TagsHolder)
for tag, value := range wafResult.Derivatives {
op.AddSerializableTag(tag, value)
}
Expand All @@ -166,7 +166,7 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat

if _, ok := l.addresses[ServerRequestBodyAddr]; ok {
dyngo.On(op, func(sdkBodyOp *types.SDKBodyOperation, args types.SDKBodyOperationArgs) {
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: map[string]any{ServerRequestBodyAddr: args.Body}}, l.config.WAFTimeout)
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: map[string]any{ServerRequestBodyAddr: args.Body}}, l.config.WAFTimeout, &op.TagsHolder)
for tag, value := range wafResult.Derivatives {
op.AddSerializableTag(tag, value)
}
Expand All @@ -193,7 +193,7 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat

// Run the WAF, ignoring the returned actions - if any - since blocking after the request handler's
// response is not supported at the moment.
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout)
wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, l.config.WAFTimeout, &op.TagsHolder)

// Add WAF metrics.
overallRuntimeNs, internalRuntimeNs := wafCtx.TotalRuntime()
Expand Down
28 changes: 27 additions & 1 deletion internal/appsec/listener/sharedsec/shared.go
Expand Up @@ -7,6 +7,8 @@

import (
"encoding/json"
"fmt"
"slices"

Check failure on line 11 in internal/appsec/listener/sharedsec/shared.go

View workflow job for this annotation

GitHub Actions / PR Unit and Integration Tests / test-core

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/slices)

Check failure on line 11 in internal/appsec/listener/sharedsec/shared.go

View workflow job for this annotation

GitHub Actions / PR Unit and Integration Tests / test-core

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/slices)

Check failure on line 11 in internal/appsec/listener/sharedsec/shared.go

View workflow job for this annotation

GitHub Actions / macos-11 go1.20

package slices is not in GOROOT (/Users/runner/hostedtoolcache/go/1.20.13/x64/src/slices)

Check failure on line 11 in internal/appsec/listener/sharedsec/shared.go

View workflow job for this annotation

GitHub Actions / macos-14 go1.20

package slices is not in GOROOT (/Users/runner/hostedtoolcache/go/1.20.13/x64/src/slices)

Check failure on line 11 in internal/appsec/listener/sharedsec/shared.go

View workflow job for this annotation

GitHub Actions / PR Unit and Integration Tests / test-contrib

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/slices)

Check failure on line 11 in internal/appsec/listener/sharedsec/shared.go

View workflow job for this annotation

GitHub Actions / PR Unit and Integration Tests / test-contrib

package slices is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/slices)
"time"

"github.com/DataDog/appsec-internal-go/limiter"
Expand All @@ -15,15 +17,39 @@
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/sharedsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/trace"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry"
)

func RunWAF(wafCtx *waf.Context, values waf.RunAddressData, timeout time.Duration) waf.Result {
func RunWAF(wafCtx *waf.Context, values waf.RunAddressData, timeout time.Duration, tags trace.TagSetter) waf.Result {
result, err := wafCtx.Run(values, timeout)
if err == waf.ErrTimeout {
log.Debug("appsec: waf timeout value of %s reached", timeout)
} else if err != nil {
log.Error("appsec: unexpected waf error: %v", err)
}

trunc := 0 // Combined trucation reason flags
for reason, values := range result.Truncations {
if len(values) == 0 {
continue
}
val := slices.Max(values)
telemetry.GlobalClient.Record(
telemetry.NamespaceAppSec,
telemetry.MetricKindDist,
"truncated_value_size",
float64(val),
[]string{
fmt.Sprintf("truncation_reason:%d", reason),
},
true, // Not language-specific
)
trunc |= int(reason)
}
if trunc != 0 && tags != nil {
tags.SetTag("_dd.appsec.waf.input_truncated", trunc)
}

return result
}

Expand Down
44 changes: 44 additions & 0 deletions internal/appsec/listener/sharedsec/shared_test.go
Expand Up @@ -6,8 +6,12 @@
package sharedsec

import (
"fmt"
"strings"
"testing"
"time"

"github.com/DataDog/appsec-internal-go/appsec"
waf "github.com/DataDog/go-libddwaf/v2"
"github.com/stretchr/testify/require"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/trace"
Expand Down Expand Up @@ -36,3 +40,43 @@ func TestTagsTypes(t *testing.T) {
require.Contains(t, tags, tag)
}
}

func TestTelemetryMetrics(t *testing.T) {
if ok, _ := waf.Health(); !ok {
// The WAF is not available, so this test is not relevant
t.SkipNow()
}

rules, err := appsec.DefaultRulesetMap()
require.NoError(t, err)
handle, err := waf.NewHandle(rules, appsec.DefaultObfuscatorKeyRegex, appsec.DefaultObfuscatorValueRegex)
require.NoError(t, err)
defer handle.Close()

wafCtx := waf.NewContext(handle)
defer wafCtx.Close()

holder := trace.NewTagsHolder()
_ = RunWAF(wafCtx, waf.RunAddressData{
Ephemeral: map[string]any{
"my.large.string": strings.Repeat("a", 8_192),
"my.large.list": make([]bool, 4_096),
"my.deep.object": makeDeep(30),
},
}, time.Minute, &holder)

tags := holder.Tags()

require.Equal(t, map[string]any{
"_dd.appsec.waf.input_truncated": int(waf.StringTooLong | waf.ObjectTooDeep | waf.ContainerTooLarge),
}, tags)
}

func makeDeep(depth int) map[string]any {
if depth <= 0 {
return nil
}
return map[string]any{
fmt.Sprintf("depth:%d", depth): makeDeep(depth - 1),
}
}
4 changes: 2 additions & 2 deletions internal/appsec/remoteconfig_test.go
Expand Up @@ -779,7 +779,7 @@ func TestWafRCUpdate(t *testing.T) {
httpsec.ServerRequestPathParamsAddr: "/rfiinc.txt",
}
// Make sure the rule matches as expected
result := sharedsec.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, cfg.WAFTimeout)
result := sharedsec.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}, cfg.WAFTimeout, nil)
require.Contains(t, jsonString(t, result.Events), "crs-913-120")
require.Empty(t, result.Actions)
// Simulate an RC update that disables the rule
Expand All @@ -795,7 +795,7 @@ func TestWafRCUpdate(t *testing.T) {
newWafCtx := waf.NewContext(newWafHandle)
defer newWafCtx.Close()
// Make sure the rule returns a blocking action when matching
result = sharedsec.RunWAF(newWafCtx, waf.RunAddressData{Persistent: values}, cfg.WAFTimeout)
result = sharedsec.RunWAF(newWafCtx, waf.RunAddressData{Persistent: values}, cfg.WAFTimeout, nil)
require.Contains(t, jsonString(t, result.Events), "crs-913-120")
require.Contains(t, result.Actions, "block")
})
Expand Down