diff --git a/appsec/events/block.go b/appsec/events/block.go new file mode 100644 index 0000000000..8db51b8268 --- /dev/null +++ b/appsec/events/block.go @@ -0,0 +1,33 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2022 Datadog, Inc. + +// Package events provides security event types that appsec can return in function calls it monitors when blocking them. +// It allows finer-grained integrations of appsec into your Go errors' management logic. +package events + +import "errors" + +var _ error = (*BlockingSecurityEvent)(nil) + +var securityError = &BlockingSecurityEvent{} + +// BlockingSecurityEvent is the error type returned by function calls blocked by appsec. +// Even though appsec takes care of responding automatically to the blocked requests, it +// is your duty to abort the request handlers that are calling functions blocked by appsec. +// For instance, if a gRPC handler performs a SQL query blocked by appsec, the SQL query +// function call gets blocked and aborted by returning an error of type SecurityBlockingEvent. +// This allows you to safely abort your request handlers, and to be able to leverage errors.As if +// necessary in your Go error management logic to be able to tell if the error is a blocking security +// event or not (eg. to avoid retrying an HTTP client request). +type BlockingSecurityEvent struct{} + +func (*BlockingSecurityEvent) Error() string { + return "request blocked by WAF" +} + +// IsSecurityError returns true if the error is a security event. +func IsSecurityError(err error) bool { + return errors.Is(err, securityError) +} diff --git a/contrib/google.golang.org/grpc/appsec.go b/contrib/google.golang.org/grpc/appsec.go index 227beb136d..108d900838 100644 --- a/contrib/google.golang.org/grpc/appsec.go +++ b/contrib/google.golang.org/grpc/appsec.go @@ -62,11 +62,13 @@ func appsecUnaryHandlerMiddleware(method string, span ddtrace.Span, handler grpc return nil, err } defer grpcsec.StartReceiveOperation(types.ReceiveOperationArgs{}, op).Finish(types.ReceiveOperationRes{Message: req}) - rv, err := handler(ctx, req) - if e, ok := err.(*types.MonitoringError); ok { - err = status.Error(codes.Code(e.GRPCStatus()), e.Error()) + + rv, downstreamErr := handler(ctx, req) + if blocked { + return nil, err } - return rv, err + + return rv, downstreamErr } } @@ -113,11 +115,12 @@ func appsecStreamHandlerMiddleware(method string, span ddtrace.Span, handler grp return err } - err = handler(srv, stream) - if e, ok := err.(*types.MonitoringError); ok { - err = status.Error(codes.Code(e.GRPCStatus()), e.Error()) + downstreamErr := handler(srv, stream) + if blocked { + return err } - return err + + return downstreamErr } } diff --git a/contrib/labstack/echo.v4/appsec.go b/contrib/labstack/echo.v4/appsec.go index 2f820551cc..9cd849cc20 100644 --- a/contrib/labstack/echo.v4/appsec.go +++ b/contrib/labstack/echo.v4/appsec.go @@ -8,9 +8,9 @@ package echo import ( "net/http" + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec" - "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec/types" "github.com/labstack/echo/v4" ) @@ -27,7 +27,7 @@ func withAppSec(next echo.HandlerFunc, span tracer.Span) echo.HandlerFunc { err = next(c) // If the error is a monitoring one, it means appsec actions will take care of writing the response // and handling the error. Don't call the echo error handler in this case - if _, ok := err.(*types.MonitoringError); !ok && err != nil { + if _, ok := err.(*events.BlockingSecurityEvent); !ok && err != nil { c.Error(err) } }) diff --git a/contrib/net/http/roundtripper.go b/contrib/net/http/roundtripper.go index 20ba543ddd..bd71daa801 100644 --- a/contrib/net/http/roundtripper.go +++ b/contrib/net/http/roundtripper.go @@ -7,6 +7,7 @@ package http import ( "fmt" + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" "math" "net/http" "os" @@ -15,6 +16,8 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec" ) type roundTripper struct { @@ -57,7 +60,7 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er if rt.cfg.after != nil { rt.cfg.after(res, span) } - if rt.cfg.errCheck == nil || rt.cfg.errCheck(err) { + if !events.IsSecurityError(err) && (rt.cfg.errCheck == nil || rt.cfg.errCheck(err)) { span.Finish(tracer.WithError(err)) } else { span.Finish() @@ -75,7 +78,15 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (res *http.Response, err er fmt.Fprintf(os.Stderr, "contrib/net/http.Roundtrip: failed to inject http headers: %v\n", err) } } + + if appsec.RASPEnabled() { + if err := httpsec.ProtectRoundTrip(ctx, r2.URL.String()); err != nil { + return nil, err + } + } + res, err = rt.base.RoundTrip(r2) + if err != nil { span.SetTag("http.errors", err.Error()) if rt.cfg.errCheck == nil || rt.cfg.errCheck(err) { diff --git a/contrib/net/http/roundtripper_test.go b/contrib/net/http/roundtripper_test.go index 5528799f19..dd7c9301be 100644 --- a/contrib/net/http/roundtripper_test.go +++ b/contrib/net/http/roundtripper_test.go @@ -16,11 +16,14 @@ import ( "testing" "time" + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/namingschematest" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec" "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" "github.com/stretchr/testify/assert" @@ -619,3 +622,78 @@ func TestClientNamingSchema(t *testing.T) { t.Run("ServiceName", namingschematest.NewServiceNameTest(genSpans, wantServiceNameV0)) t.Run("SpanName", namingschematest.NewSpanNameTest(genSpans, assertOpV0, assertOpV1)) } + +type emptyRoundTripper struct{} + +func (rt *emptyRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { + recorder := httptest.NewRecorder() + recorder.WriteHeader(200) + return recorder.Result(), nil +} + +func TestAppsec(t *testing.T) { + t.Setenv("DD_APPSEC_RULES", "../../../internal/appsec/testdata/rasp.json") + + client := WrapRoundTripper(&emptyRoundTripper{}) + + for _, enabled := range []bool{true, false} { + + t.Run(strconv.FormatBool(enabled), func(t *testing.T) { + t.Setenv("DD_APPSEC_RASP_ENABLED", strconv.FormatBool(enabled)) + + mt := mocktracer.Start() + defer mt.Stop() + + appsec.Start() + if !appsec.Enabled() { + t.Skip("appsec not enabled") + } + + defer appsec.Stop() + + w := httptest.NewRecorder() + r, err := http.NewRequest("GET", "?value=169.254.169.254", nil) + require.NoError(t, err) + + TraceAndServe(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req, err := http.NewRequest("GET", "http://169.254.169.254", nil) + require.NoError(t, err) + + resp, err := client.RoundTrip(req.WithContext(r.Context())) + + if enabled { + require.ErrorIs(t, err, &events.BlockingSecurityEvent{}) + } else { + require.NoError(t, err) + } + + if resp != nil { + defer resp.Body.Close() + } + }), w, r, &ServeConfig{ + Service: "service", + Resource: "resource", + }) + + spans := mt.FinishedSpans() + require.Len(t, spans, 2) // service entry serviceSpan & http request serviceSpan + serviceSpan := spans[1] + + if !enabled { + require.NotContains(t, serviceSpan.Tags(), "_dd.appsec.json") + require.NotContains(t, serviceSpan.Tags(), "_dd.stack") + return + } + + require.Contains(t, serviceSpan.Tags(), "_dd.appsec.json") + appsecJSON := serviceSpan.Tag("_dd.appsec.json") + require.Contains(t, appsecJSON, httpsec.ServerIoNetURLAddr) + + require.Contains(t, serviceSpan.Tags(), "_dd.stack") + + // This is a nested event so it should contain the child span id in the service entry span + // TODO(eliott.bouhana): uncomment this once we have the child span id in the service entry span + // require.Contains(t, appsecJSON, `"span_id":`+strconv.FormatUint(requestSpan.SpanID(), 10)) + }) + } +} diff --git a/go.mod b/go.mod index 37ea06e17a..badc5bb218 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.20 require ( cloud.google.com/go/pubsub v1.33.0 github.com/99designs/gqlgen v0.17.36 - github.com/DataDog/appsec-internal-go v1.5.0 + github.com/DataDog/appsec-internal-go v1.6.0 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 diff --git a/go.sum b/go.sum index 2f0c0289fa..ca92373874 100644 --- a/go.sum +++ b/go.sum @@ -624,8 +624,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9s github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno= -github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= +github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw= +github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo= github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c= diff --git a/internal/apps/go.mod b/internal/apps/go.mod index 0c24d7944b..558f1fd6ce 100644 --- a/internal/apps/go.mod +++ b/internal/apps/go.mod @@ -8,7 +8,7 @@ require ( ) require ( - github.com/DataDog/appsec-internal-go v1.5.0 // indirect + github.com/DataDog/appsec-internal-go v1.6.0 // indirect github.com/DataDog/go-libddwaf/v3 v3.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect diff --git a/internal/apps/go.sum b/internal/apps/go.sum index 12ef104219..e5ce401622 100644 --- a/internal/apps/go.sum +++ b/internal/apps/go.sum @@ -1,5 +1,5 @@ -github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno= -github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= +github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw= +github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo= github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c= diff --git a/internal/appsec/appsec.go b/internal/appsec/appsec.go index 17d7846870..ae68d0d668 100644 --- a/internal/appsec/appsec.go +++ b/internal/appsec/appsec.go @@ -26,6 +26,13 @@ func Enabled() bool { return activeAppSec != nil && activeAppSec.started } +// RASPEnabled returns true when DD_APPSEC_RASP_ENABLED=true or is unset. Granted that AppSec is enabled. +func RASPEnabled() bool { + mu.RLock() + defer mu.RUnlock() + return activeAppSec != nil && activeAppSec.started && activeAppSec.cfg.RASP +} + // Start AppSec when enabled is enabled by both using the appsec build tag and // setting the environment variable DD_APPSEC_ENABLED to true. func Start(opts ...config.StartOption) { @@ -162,6 +169,7 @@ func (a *appsec) start(telemetry *appsecTelemetry) error { } a.enableRCBlocking() + a.enableRASP() a.started = true log.Info("appsec: up and running") diff --git a/internal/appsec/config/config.go b/internal/appsec/config/config.go index 33ca45e56b..e2a0b7736a 100644 --- a/internal/appsec/config/config.go +++ b/internal/appsec/config/config.go @@ -66,7 +66,8 @@ type Config struct { // APISec configuration APISec internal.APISecConfig // RC is the remote configuration client used to receive product configuration updates. Nil if RC is disabled (default) - RC *remoteconfig.ClientConfig + RC *remoteconfig.ClientConfig + RASP bool } // WithRCConfig sets the AppSec remote config client configuration to the specified cfg @@ -115,5 +116,6 @@ func NewConfig() (*Config, error) { TraceRateLimit: int64(internal.RateLimitFromEnv()), Obfuscator: internal.NewObfuscatorConfig(), APISec: internal.NewAPISecConfig(), + RASP: internal.RASPEnabled(), }, nil } diff --git a/internal/appsec/emitter/grpcsec/types/types.go b/internal/appsec/emitter/grpcsec/types/types.go index 6e94ef9495..449ce2fbc3 100644 --- a/internal/appsec/emitter/grpcsec/types/types.go +++ b/internal/appsec/emitter/grpcsec/types/types.go @@ -72,33 +72,8 @@ type ( // Corresponds to the address `grpc.server.request.message`. Message interface{} } - - // MonitoringError is used to vehicle a gRPC error that also embeds a request status code - MonitoringError struct { - msg string - status uint32 - } ) -// NewMonitoringError creates and returns a new gRPC monitoring error, wrapped under -// sharedesec.MonitoringError -func NewMonitoringError(msg string, code uint32) error { - return &MonitoringError{ - msg: msg, - status: code, - } -} - -// GRPCStatus returns the gRPC status code embedded in the error -func (e *MonitoringError) GRPCStatus() uint32 { - return e.status -} - -// Error implements the error interface -func (e *MonitoringError) Error() string { - return e.msg -} - // Finish the gRPC handler operation, along with the given results, and emit a // finish event up in the operation stack. func (op *HandlerOperation) Finish(res HandlerOperationRes) []any { diff --git a/internal/appsec/emitter/httpsec/http.go b/internal/appsec/emitter/httpsec/http.go index 78d51e5cb8..2f7ec76e49 100644 --- a/internal/appsec/emitter/httpsec/http.go +++ b/internal/appsec/emitter/httpsec/http.go @@ -12,6 +12,7 @@ package httpsec import ( "context" + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" // Blank import needed to use embed for the default blocked response payloads _ "embed" @@ -26,6 +27,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/trace" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/trace/httptrace" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/stacktrace" "github.com/DataDog/appsec-internal-go/netip" ) @@ -34,7 +36,7 @@ import ( // This function should not be called when AppSec is disabled in order to // get preciser error logs. func MonitorParsedBody(ctx context.Context, body any) error { - parent := fromContext(ctx) + parent, _ := ctx.Value(listener.ContextKey{}).(*types.Operation) if parent == nil { log.Error("appsec: parsed http body monitoring ignored: could not find the http handler instrumentation metadata in the request context: the request handler is not being monitored by a middleware function or the provided context is not the expected request context") return nil @@ -48,7 +50,7 @@ func MonitorParsedBody(ctx context.Context, body any) error { func ExecuteSDKBodyOperation(parent dyngo.Operation, args types.SDKBodyOperationArgs) error { var err error op := &types.SDKBodyOperation{Operation: dyngo.NewOperation(parent)} - dyngo.OnData(op, func(e error) { + dyngo.OnData(op, func(e *events.BlockingSecurityEvent) { err = e }) dyngo.StartOperation(op, args) @@ -78,6 +80,7 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string] var bypassHandler http.Handler var blocking bool + var stackTrace *stacktrace.Event args := MakeHandlerOperationArgs(r, clientIP, pathParams) ctx, op := StartOperation(r.Context(), args, func(op *types.Operation) { dyngo.OnData(op, func(a *sharedsec.HTTPAction) { @@ -85,7 +88,7 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string] bypassHandler = a.Handler }) dyngo.OnData(op, func(a *sharedsec.StackTraceAction) { - // TODO: do something with the stacktrace + stackTrace = &a.Event }) }) r = r.WithContext(ctx) @@ -102,6 +105,11 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string] } } + // Add stacktraces to the span, if any + if stackTrace != nil { + stacktrace.AddToSpan(span, stackTrace) + } + if bypassHandler != nil { bypassHandler.ServeHTTP(w, r) } @@ -194,10 +202,3 @@ func StartOperation(ctx context.Context, args types.HandlerOperationArgs, setup dyngo.StartOperation(op, args) return newCtx, op } - -// fromContext returns the Operation object stored in the context, if any -func fromContext(ctx context.Context) *types.Operation { - // Avoid a runtime panic in case of type-assertion error by collecting the 2 return values - op, _ := ctx.Value(listener.ContextKey{}).(*types.Operation) - return op -} diff --git a/internal/appsec/emitter/httpsec/roundtripper.go b/internal/appsec/emitter/httpsec/roundtripper.go new file mode 100644 index 0000000000..99e248fe6c --- /dev/null +++ b/internal/appsec/emitter/httpsec/roundtripper.go @@ -0,0 +1,55 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package httpsec + +import ( + "context" + "sync" + + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec/types" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +var badInputContextOnce sync.Once + +func ProtectRoundTrip(ctx context.Context, url string) error { + opArgs := types.RoundTripOperationArgs{ + URL: url, + } + + parent, _ := ctx.Value(listener.ContextKey{}).(dyngo.Operation) + if parent == nil { // No parent operation => we can't monitor the request + badInputContextOnce.Do(func() { + log.Debug("appsec: outgoing http request monitoring ignored: could not find the handler " + + "instrumentation metadata in the request context: the request handler is not being monitored by a " + + "middleware function or the incoming request context has not be forwarded correctly to the roundtripper") + }) + return nil + } + + op := &types.RoundTripOperation{ + Operation: dyngo.NewOperation(parent), + } + + var err *events.BlockingSecurityEvent + // TODO: move the data listener as a setup function of httpsec.StartRoundTripperOperation(ars, ) + dyngo.OnData(op, func(e *events.BlockingSecurityEvent) { + err = e + }) + + dyngo.StartOperation(op, opArgs) + dyngo.FinishOperation(op, types.RoundTripOperationRes{}) + + if err != nil { + log.Debug("appsec: outgoing http request blocked by the WAF on URL: %s", url) + return err + } + + return nil +} diff --git a/internal/appsec/emitter/httpsec/types/types.go b/internal/appsec/emitter/httpsec/types/types.go index 04e481c124..2ea8648b7c 100644 --- a/internal/appsec/emitter/httpsec/types/types.go +++ b/internal/appsec/emitter/httpsec/types/types.go @@ -27,6 +27,10 @@ type ( SDKBodyOperation struct { dyngo.Operation } + + RoundTripOperation struct { + dyngo.Operation + } ) // Finish the HTTP handler operation, along with the given results and emits a @@ -66,16 +70,20 @@ type ( // SDKBodyOperationArgs is the SDK body operation arguments. SDKBodyOperationArgs struct { // Body corresponds to the address `server.request.body`. - Body interface{} + Body any } // SDKBodyOperationRes is the SDK body operation results. SDKBodyOperationRes struct{} - // MonitoringError is used to vehicle an HTTP error, usually resurfaced through Appsec SDKs. - MonitoringError struct { - msg string + // RoundTripOperationArgs is the round trip operation arguments. + RoundTripOperationArgs struct { + // URL corresponds to the address `server.io.net.url`. + URL string } + + // RoundTripOperationRes is the round trip operation results. + RoundTripOperationRes struct{} ) // Finish finishes the SDKBody operation and emits a finish event @@ -83,21 +91,11 @@ func (op *SDKBodyOperation) Finish() { dyngo.FinishOperation(op, SDKBodyOperationRes{}) } -// Error implements the Error interface -func (e *MonitoringError) Error() string { - return e.msg -} - -// NewMonitoringError creates and returns a new HTTP monitoring error, wrapped under -// sharedesec.MonitoringError -func NewMonitoringError(msg string) error { - return &MonitoringError{ - msg: msg, - } -} - func (SDKBodyOperationArgs) IsArgOf(*SDKBodyOperation) {} func (SDKBodyOperationRes) IsResultOf(*SDKBodyOperation) {} func (HandlerOperationArgs) IsArgOf(*Operation) {} func (HandlerOperationRes) IsResultOf(*Operation) {} + +func (RoundTripOperationArgs) IsArgOf(*RoundTripOperation) {} +func (RoundTripOperationRes) IsResultOf(*RoundTripOperation) {} diff --git a/internal/appsec/emitter/sharedsec/actions.go b/internal/appsec/emitter/sharedsec/actions.go index abbab10a0f..0a9c0cd3b8 100644 --- a/internal/appsec/emitter/sharedsec/actions.go +++ b/internal/appsec/emitter/sharedsec/actions.go @@ -7,11 +7,11 @@ package sharedsec import ( _ "embed" // Blank import - "errors" "net/http" "os" "strings" + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/stacktrace" @@ -103,7 +103,7 @@ func (a *StackTraceAction) EmitData(op dyngo.Operation) { dyngo.EmitData(op, a) func NewStackTraceAction(params map[string]any) Action { id, ok := params["stack_id"] if !ok { - log.Debug("appsec: could not read stack_id parameter for stack_trace action") + log.Debug("appsec: could not read stack_id parameter for generate_stack action") return nil } @@ -197,7 +197,7 @@ func newBlockRequestHandler(status int, ct string, payload []byte) http.Handler func newGRPCBlockHandler(status int) GRPCWrapper { return func(_ map[string][]string) (uint32, error) { - return uint32(status), errors.New("Request blocked") + return uint32(status), &events.BlockingSecurityEvent{} } } diff --git a/internal/appsec/emitter/sharedsec/shared.go b/internal/appsec/emitter/sharedsec/shared.go index 715afc45cd..440fe47839 100644 --- a/internal/appsec/emitter/sharedsec/shared.go +++ b/internal/appsec/emitter/sharedsec/shared.go @@ -7,6 +7,7 @@ package sharedsec import ( "context" + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" "reflect" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" @@ -39,7 +40,7 @@ var userIDOperationArgsType = reflect.TypeOf((*UserIDOperationArgs)(nil)).Elem() func ExecuteUserIDOperation(parent dyngo.Operation, args UserIDOperationArgs) error { var err error op := &UserIDOperation{Operation: dyngo.NewOperation(parent)} - dyngo.OnData(op, func(e error) { err = e }) + dyngo.OnData(op, func(e *events.BlockingSecurityEvent) { err = e }) dyngo.StartOperation(op, args) dyngo.FinishOperation(op, UserIDOperationRes{}) return err diff --git a/internal/appsec/listener/graphqlsec/graphql.go b/internal/appsec/listener/graphqlsec/graphql.go index 03f82cac50..9a1ef48b2c 100644 --- a/internal/appsec/listener/graphqlsec/graphql.go +++ b/internal/appsec/listener/graphqlsec/graphql.go @@ -8,17 +8,19 @@ package graphqlsec import ( "sync" - "github.com/DataDog/appsec-internal-go/limiter" - waf "github.com/DataDog/go-libddwaf/v3" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/graphqlsec/types" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec" shared "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/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/samplernames" + + "github.com/DataDog/appsec-internal-go/limiter" + waf "github.com/DataDog/go-libddwaf/v3" ) // GraphQL rule addresses currently supported by the WAF @@ -28,7 +30,8 @@ const ( // List of GraphQL rule addresses currently supported by the WAF var supportedAddresses = listener.AddressSet{ - graphQLServerResolverAddr: {}, + graphQLServerResolverAddr: {}, + httpsec.ServerIoNetURLAddr: {}, } // Install registers the GraphQL WAF Event Listener on the given root operation. @@ -83,6 +86,10 @@ func (l *wafEventListener) onEvent(request *types.RequestOperation, _ types.Requ return } + if _, ok := l.addresses[httpsec.ServerIoNetURLAddr]; ok { + httpsec.RegisterRoundTripperListener(request, &request.SecurityEventsHolder, wafCtx, l.limiter) + } + // Add span tags notifying this trace is AppSec-enabled trace.SetAppSecEnabledTags(request) l.once.Do(func() { @@ -101,7 +108,7 @@ func (l *wafEventListener) onEvent(request *types.RequestOperation, _ types.Requ }, }, ) - shared.AddSecurityEvents(field, l.limiter, wafResult.Events) + shared.AddSecurityEvents(&field.SecurityEventsHolder, l.limiter, wafResult.Events) } dyngo.OnFinish(field, func(field *types.ResolveOperation, res types.ResolveOperationRes) { diff --git a/internal/appsec/listener/grpcsec/grpc.go b/internal/appsec/listener/grpcsec/grpc.go index a258410766..3dc4e16154 100644 --- a/internal/appsec/listener/grpcsec/grpc.go +++ b/internal/appsec/listener/grpcsec/grpc.go @@ -10,9 +10,6 @@ import ( "go.uber.org/atomic" - "github.com/DataDog/appsec-internal-go/limiter" - waf "github.com/DataDog/go-libddwaf/v3" - "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" @@ -23,6 +20,9 @@ import ( shared "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sharedsec" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/samplernames" + + "github.com/DataDog/appsec-internal-go/limiter" + waf "github.com/DataDog/go-libddwaf/v3" ) // gRPC rule addresses currently supported by the WAF @@ -30,8 +30,6 @@ const ( GRPCServerMethodAddr = "grpc.server.method" GRPCServerRequestMessageAddr = "grpc.server.request.message" GRPCServerRequestMetadataAddr = "grpc.server.request.metadata" - HTTPClientIPAddr = httpsec.HTTPClientIPAddr - UserIDAddr = httpsec.UserIDAddr ) // List of gRPC rule addresses currently supported by the WAF @@ -39,8 +37,9 @@ var supportedAddresses = listener.AddressSet{ GRPCServerMethodAddr: {}, GRPCServerRequestMessageAddr: {}, GRPCServerRequestMetadataAddr: {}, - HTTPClientIPAddr: {}, - UserIDAddr: {}, + httpsec.HTTPClientIPAddr: {}, + httpsec.UserIDAddr: {}, + httpsec.ServerIoNetURLAddr: {}, } // Install registers the gRPC WAF Event Listener on the given root operation. @@ -106,26 +105,23 @@ func (l *wafEventListener) onEvent(op *types.HandlerOperation, handlerArgs types return } + if _, ok := l.addresses[httpsec.ServerIoNetURLAddr]; ok { + httpsec.RegisterRoundTripperListener(op, &op.SecurityEventsHolder, wafCtx, l.limiter) + } + // Listen to the UserID address if the WAF rules are using it - if l.isSecAddressListened(UserIDAddr) { + if l.isSecAddressListened(httpsec.UserIDAddr) { // UserIDOperation happens when appsec.SetUser() is called. We run the WAF and apply actions to // 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(userIDOp *sharedsec.UserIDOperation, args sharedsec.UserIDOperationArgs) { values := map[string]any{ - UserIDAddr: args.UserID, + httpsec.UserIDAddr: args.UserID, } wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}) if wafResult.HasActions() || wafResult.HasEvents() { - for aType, params := range wafResult.Actions { - for _, action := range shared.ActionsFromEntry(aType, params) { - if grpcAction, ok := action.(*sharedsec.GRPCAction); ok { - code, err := grpcAction.GRPCWrapper(map[string][]string{}) - dyngo.EmitData(userIDOp, types.NewMonitoringError(err.Error(), code)) - } - } - } - shared.AddSecurityEvents(op, l.limiter, wafResult.Events) + shared.ProcessActions(userIDOp, wafResult.Actions) + shared.AddSecurityEvents(&op.SecurityEventsHolder, l.limiter, wafResult.Events) log.Debug("appsec: WAF detected an authenticated user attack: %s", args.UserID) } }) @@ -136,14 +132,14 @@ func (l *wafEventListener) onEvent(op *types.HandlerOperation, handlerArgs types // Note that this address is passed asap for the passlist, which are created per grpc method values[GRPCServerMethodAddr] = handlerArgs.Method } - if l.isSecAddressListened(HTTPClientIPAddr) && handlerArgs.ClientIP.IsValid() { - values[HTTPClientIPAddr] = handlerArgs.ClientIP.String() + if l.isSecAddressListened(httpsec.HTTPClientIPAddr) && handlerArgs.ClientIP.IsValid() { + values[httpsec.HTTPClientIPAddr] = handlerArgs.ClientIP.String() } wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: values}) if wafResult.HasActions() || wafResult.HasEvents() { - interrupt := shared.ProcessActions(op, wafResult.Actions, nil) - shared.AddSecurityEvents(op, l.limiter, wafResult.Events) + interrupt := shared.ProcessActions(op, wafResult.Actions) + shared.AddSecurityEvents(&op.SecurityEventsHolder, l.limiter, wafResult.Events) log.Debug("appsec: WAF detected an attack before executing the request") if interrupt { wafCtx.Close() @@ -199,7 +195,7 @@ func (l *wafEventListener) onEvent(op *types.HandlerOperation, handlerArgs types op.SetTag(ext.ManualKeep, samplernames.AppSec) }) - shared.AddSecurityEvents(op, l.limiter, events) + shared.AddSecurityEvents(&op.SecurityEventsHolder, l.limiter, events) }) } diff --git a/internal/appsec/listener/httpsec/http.go b/internal/appsec/listener/httpsec/http.go index 12d0038d9e..1d9c750e69 100644 --- a/internal/appsec/listener/httpsec/http.go +++ b/internal/appsec/listener/httpsec/http.go @@ -10,8 +10,6 @@ import ( "math/rand" "sync" - "github.com/DataDog/appsec-internal-go/limiter" - waf "github.com/DataDog/go-libddwaf/v3" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" @@ -21,6 +19,9 @@ import ( shared "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sharedsec" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" "gopkg.in/DataDog/dd-trace-go.v1/internal/samplernames" + + "github.com/DataDog/appsec-internal-go/limiter" + waf "github.com/DataDog/go-libddwaf/v3" ) // HTTP rule addresses currently supported by the WAF @@ -36,6 +37,7 @@ const ( ServerResponseHeadersNoCookiesAddr = "server.response.headers.no_cookies" HTTPClientIPAddr = "http.client_ip" UserIDAddr = "usr.id" + ServerIoNetURLAddr = "server.io.net.url" ) // List of HTTP rule addresses currently supported by the WAF @@ -51,6 +53,7 @@ var supportedAddresses = listener.AddressSet{ ServerResponseHeadersNoCookiesAddr: {}, HTTPClientIPAddr: {}, UserIDAddr: {}, + ServerIoNetURLAddr: {}, } // Install registers the HTTP WAF Event Listener on the given root operation. @@ -70,6 +73,7 @@ type wafEventListener struct { once sync.Once } +// newWAFEventListener returns the WAF event listener to register in order to enable it. func newWafEventListener(wafHandle *waf.Handle, cfg *config.Config, limiter limiter.Limiter) *wafEventListener { if wafHandle == nil { log.Debug("appsec: no WAF Handle available, the HTTP WAF Event Listener will not be registered") @@ -91,7 +95,6 @@ func newWafEventListener(wafHandle *waf.Handle, cfg *config.Config, limiter limi } } -// NewWAFEventListener returns the WAF event listener to register in order to enable it. func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperationArgs) { wafCtx, err := l.wafHandle.NewContextWithBudget(l.config.WAFTimeout) if err != nil { @@ -105,6 +108,10 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat return } + if _, ok := l.addresses[ServerIoNetURLAddr]; ok { + RegisterRoundTripperListener(op, &op.SecurityEventsHolder, wafCtx, l.limiter) + } + if _, ok := l.addresses[UserIDAddr]; ok { // OnUserIDOperationStart happens when appsec.SetUser() is called. We run the WAF and apply actions to // see if the associated user should be blocked. Since we don't control the execution flow in this case @@ -112,8 +119,8 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat dyngo.On(op, func(operation *sharedsec.UserIDOperation, args sharedsec.UserIDOperationArgs) { wafResult := shared.RunWAF(wafCtx, waf.RunAddressData{Persistent: map[string]any{UserIDAddr: args.UserID}}) if wafResult.HasActions() || wafResult.HasEvents() { - shared.ProcessActions(operation, wafResult.Actions, types.NewMonitoringError("Request blocked")) - shared.AddSecurityEvents(op, l.limiter, wafResult.Events) + shared.ProcessActions(operation, wafResult.Actions) + shared.AddSecurityEvents(&op.SecurityEventsHolder, l.limiter, wafResult.Events) log.Debug("appsec: WAF detected a suspicious user: %s", args.UserID) } }) @@ -159,8 +166,8 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat op.AddSerializableTag(tag, value) } if wafResult.HasActions() || wafResult.HasEvents() { - interrupt := shared.ProcessActions(op, wafResult.Actions, nil) - shared.AddSecurityEvents(op, l.limiter, wafResult.Events) + interrupt := shared.ProcessActions(op, wafResult.Actions) + shared.AddSecurityEvents(&op.SecurityEventsHolder, l.limiter, wafResult.Events) log.Debug("appsec: WAF detected an attack before executing the request") if interrupt { wafCtx.Close() @@ -175,8 +182,8 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat op.AddSerializableTag(tag, value) } if wafResult.HasActions() || wafResult.HasEvents() { - shared.ProcessActions(sdkBodyOp, wafResult.Actions, types.NewMonitoringError("Request blocked")) - shared.AddSecurityEvents(op, l.limiter, wafResult.Events) + shared.ProcessActions(sdkBodyOp, wafResult.Actions) + shared.AddSecurityEvents(&op.SecurityEventsHolder, l.limiter, wafResult.Events) log.Debug("appsec: WAF detected a suspicious request body") } }) @@ -211,7 +218,7 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat // Log the attacks if any if wafResult.HasEvents() { log.Debug("appsec: attack detected by the waf") - shared.AddSecurityEvents(op, l.limiter, wafResult.Events) + shared.AddSecurityEvents(&op.SecurityEventsHolder, l.limiter, wafResult.Events) } for tag, value := range wafResult.Derivatives { op.AddSerializableTag(tag, value) diff --git a/internal/appsec/listener/httpsec/roundtripper.go b/internal/appsec/listener/httpsec/roundtripper.go new file mode 100644 index 0000000000..e631013120 --- /dev/null +++ b/internal/appsec/listener/httpsec/roundtripper.go @@ -0,0 +1,32 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package httpsec + +import ( + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec/types" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sharedsec" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/trace" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + + "github.com/DataDog/appsec-internal-go/limiter" + "github.com/DataDog/go-libddwaf/v3" +) + +// RegisterRoundTripperListener registers a listener on outgoing HTTP client requests to run the WAF. +func RegisterRoundTripperListener(op dyngo.Operation, events *trace.SecurityEventsHolder, wafCtx *waf.Context, limiter limiter.Limiter) { + dyngo.On(op, func(op *types.RoundTripOperation, args types.RoundTripOperationArgs) { + wafResult := sharedsec.RunWAF(wafCtx, waf.RunAddressData{Persistent: map[string]any{ServerIoNetURLAddr: args.URL}}) + if !wafResult.HasEvents() { + return + } + + log.Debug("appsec: WAF detected a suspicious outgoing request URL: %s", args.URL) + + sharedsec.ProcessActions(op, wafResult.Actions) + sharedsec.AddSecurityEvents(events, limiter, wafResult.Events) + }) +} diff --git a/internal/appsec/listener/sharedsec/shared.go b/internal/appsec/listener/sharedsec/shared.go index d4773dbb1d..39f4353d41 100644 --- a/internal/appsec/listener/sharedsec/shared.go +++ b/internal/appsec/listener/sharedsec/shared.go @@ -7,43 +7,44 @@ package sharedsec import ( "encoding/json" - "github.com/DataDog/appsec-internal-go/limiter" - waf "github.com/DataDog/go-libddwaf/v3" + "errors" + + "gopkg.in/DataDog/dd-trace-go.v1/appsec/events" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" "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" + + "github.com/DataDog/appsec-internal-go/limiter" + waf "github.com/DataDog/go-libddwaf/v3" + wafErrors "github.com/DataDog/go-libddwaf/v3/errors" +) + +const ( + eventRulesVersionTag = "_dd.appsec.event_rules.version" + eventRulesErrorsTag = "_dd.appsec.event_rules.errors" + eventRulesLoadedTag = "_dd.appsec.event_rules.loaded" + eventRulesFailedTag = "_dd.appsec.event_rules.error_count" + wafVersionTag = "_dd.appsec.waf.version" ) func RunWAF(wafCtx *waf.Context, values waf.RunAddressData) waf.Result { result, err := wafCtx.Run(values) - if err == waf.ErrTimeout { - log.Debug("appsec: waf timeout value of reached: %v", err) + if errors.Is(err, wafErrors.ErrTimeout) { + log.Debug("appsec: waf timeout value reached: %v", err) } else if err != nil { log.Error("appsec: unexpected waf error: %v", err) } return result } -type securityEventsAdder interface { - AddSecurityEvents(events []any) -} - // AddSecurityEvents is a helper function to add sec events to an operation taking into account the rate limiter. -func AddSecurityEvents(op securityEventsAdder, limiter limiter.Limiter, matches []any) { +func AddSecurityEvents(holder *trace.SecurityEventsHolder, limiter limiter.Limiter, matches []any) { if len(matches) > 0 && limiter.Allow() { - op.AddSecurityEvents(matches) + holder.AddSecurityEvents(matches) } } -const ( - eventRulesVersionTag = "_dd.appsec.event_rules.version" - eventRulesErrorsTag = "_dd.appsec.event_rules.errors" - eventRulesLoadedTag = "_dd.appsec.event_rules.loaded" - eventRulesFailedTag = "_dd.appsec.event_rules.error_count" - wafVersionTag = "_dd.appsec.waf.version" -) - // AddRulesMonitoringTags adds the tags related to security rules monitoring func AddRulesMonitoringTags(th trace.TagSetter, wafDiags *waf.Diagnostics) { rInfo := wafDiags.Rules @@ -78,8 +79,9 @@ func AddWAFMonitoringTags(th trace.TagSetter, rulesVersion string, stats map[str // ProcessActions sends the relevant actions to the operation's data listener. // It returns true if at least one of those actions require interrupting the request handler // When SDKError is not nil, this error is sent to the op with EmitData so that the invoked SDK can return it -func ProcessActions(op dyngo.Operation, actions map[string]any, SDKError error) (interrupt bool) { +func ProcessActions(op dyngo.Operation, actions map[string]any) (interrupt bool) { for aType, params := range actions { + log.Debug("appsec: processing %s action with params %v", aType, params) actionArray := ActionsFromEntry(aType, params) if actionArray == nil { log.Debug("cannot process %s action with params %v", aType, params) @@ -87,12 +89,16 @@ func ProcessActions(op dyngo.Operation, actions map[string]any, SDKError error) } for _, a := range actionArray { a.EmitData(op) - if a.Blocking() && SDKError != nil { // Send the error to be returned by the SDK - interrupt = true - dyngo.EmitData(op, SDKError) // Send error - } + interrupt = interrupt || a.Blocking() } } + + // If any of the actions are supposed to interrupt the request, emit a blocking event for the SDK operations + // to return an error. + if interrupt { + dyngo.EmitData(op, &events.BlockingSecurityEvent{}) + } + return interrupt } @@ -108,7 +114,7 @@ func ActionsFromEntry(actionType string, params any) []sharedsec.Action { return sharedsec.NewBlockAction(p) case "redirect_request": return []sharedsec.Action{sharedsec.NewRedirectAction(p)} - case "stack_trace": + case "generate_stack": return []sharedsec.Action{sharedsec.NewStackTraceAction(p)} default: diff --git a/internal/appsec/remoteconfig.go b/internal/appsec/remoteconfig.go index ecaa60a2a4..9dddf65a8d 100644 --- a/internal/appsec/remoteconfig.go +++ b/internal/appsec/remoteconfig.go @@ -402,6 +402,16 @@ func (a *appsec) enableRCBlocking() { } } +func (a *appsec) enableRASP() { + if !a.cfg.RASP { + return + } + if err := remoteconfig.RegisterCapability(remoteconfig.ASMRASPSSRF); err != nil { + log.Debug("appsec: Remote config: couldn't register RASP SSRF: %v", err) + } + // TODO: register other RASP capabilities when supported +} + func (a *appsec) disableRCBlocking() { if a.cfg.RC == nil { return diff --git a/internal/appsec/testdata/rasp.json b/internal/appsec/testdata/rasp.json new file mode 100644 index 0000000000..ecbaf0d16f --- /dev/null +++ b/internal/appsec/testdata/rasp.json @@ -0,0 +1,158 @@ +{ + "version": "2.2", + "metadata": { + "rules_version": "1.4.2" + }, + "rules": [ + { + "id": "rasp-930-100", + "name": "Local file inclusion exploit", + "tags": { + "type": "lfi", + "category": "vulnerability_trigger", + "cwe": "22", + "capec": "1000/255/153/126", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.io.fs.file" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "lfi_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace", + "block" + ] + }, + { + "id": "rasp-934-100", + "name": "Server-side request forgery exploit", + "tags": { + "type": "ssrf", + "category": "vulnerability_trigger", + "cwe": "918", + "capec": "1000/225/115/664", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.io.net.url" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "grpc.server.request.message" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ] + }, + "operator": "ssrf_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace", + "block" + ] + }, + { + "id": "rasp-942-100", + "name": "SQL injection exploit", + "tags": { + "type": "sql_injection", + "category": "vulnerability_trigger", + "cwe": "89", + "capec": "1000/152/248/66", + "confidence": "0", + "module": "rasp" + }, + "conditions": [ + { + "parameters": { + "resource": [ + { + "address": "server.db.statement" + } + ], + "params": [ + { + "address": "server.request.query" + }, + { + "address": "server.request.body" + }, + { + "address": "server.request.path_params" + }, + { + "address": "graphql.server.all_resolvers" + }, + { + "address": "graphql.server.resolver" + } + ], + "db_type": [ + { + "address": "server.db.system" + } + ] + }, + "operator": "sqli_detector" + } + ], + "transformers": [], + "on_match": [ + "stack_trace", + "block" + ] + } + ], + "rules_data": [] +} diff --git a/internal/appsec/waf_test.go b/internal/appsec/waf_test.go index e3fbf3d755..c2e3d7c954 100644 --- a/internal/appsec/waf_test.go +++ b/internal/appsec/waf_test.go @@ -15,8 +15,6 @@ import ( "strings" "testing" - internal "github.com/DataDog/appsec-internal-go/appsec" - waf "github.com/DataDog/go-libddwaf/v3" pAppsec "gopkg.in/DataDog/dd-trace-go.v1/appsec" httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" @@ -24,6 +22,8 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config" "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec" + internal "github.com/DataDog/appsec-internal-go/appsec" + waf "github.com/DataDog/go-libddwaf/v3" "github.com/stretchr/testify/require" ) diff --git a/internal/exectracetest/go.mod b/internal/exectracetest/go.mod index 8a48dd3a42..4ab15fa92a 100644 --- a/internal/exectracetest/go.mod +++ b/internal/exectracetest/go.mod @@ -9,7 +9,7 @@ require ( ) require ( - github.com/DataDog/appsec-internal-go v1.5.0 // indirect + github.com/DataDog/appsec-internal-go v1.6.0 // indirect github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect github.com/DataDog/datadog-go/v5 v5.3.0 // indirect diff --git a/internal/exectracetest/go.sum b/internal/exectracetest/go.sum index f9168824e3..33151b0657 100644 --- a/internal/exectracetest/go.sum +++ b/internal/exectracetest/go.sum @@ -1,5 +1,5 @@ -github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno= -github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= +github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw= +github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo= github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c= diff --git a/internal/remoteconfig/remoteconfig.go b/internal/remoteconfig/remoteconfig.go index 5c8f026392..86b8f69231 100644 --- a/internal/remoteconfig/remoteconfig.go +++ b/internal/remoteconfig/remoteconfig.go @@ -70,6 +70,8 @@ const ( APMTracingHTTPHeaderTags // APMTracingCustomTags enables APM client to set custom tags on all spans APMTracingCustomTags + // ASMRASPSSRF enables ASM support for runtime protection against SSRF attacks + ASMRASPSSRF = 23 ) // Additional capability bit index values that are non-consecutive from above. diff --git a/internal/stacktrace/event.go b/internal/stacktrace/event.go index feb1e2c581..ac292f13f6 100644 --- a/internal/stacktrace/event.go +++ b/internal/stacktrace/event.go @@ -88,14 +88,16 @@ func AddToSpan(span ddtrace.Span, events ...*Event) { return } - groupByCategory := map[EventCategory][]*Event{ - ExceptionEvent: {}, - VulnerabilityEvent: {}, - ExploitEvent: {}, - } + // TODO(eliott.bouhana): switch to a map[EventCategory][]*Event type when the tinylib/msgp@1.1.10 is out + groupByCategory := make(map[string]any, 3) for _, event := range events { - groupByCategory[event.Category] = append(groupByCategory[event.Category], event) + if _, ok := groupByCategory[string(event.Category)]; !ok { + groupByCategory[string(event.Category)] = []*Event{event} + continue + } + + groupByCategory[string(event.Category)] = append(groupByCategory[string(event.Category)].([]*Event), event) } type rooter interface { diff --git a/internal/stacktrace/event_test.go b/internal/stacktrace/event_test.go index 5da7febd9c..a95db53633 100644 --- a/internal/stacktrace/event_test.go +++ b/internal/stacktrace/event_test.go @@ -13,6 +13,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/internal" "github.com/stretchr/testify/require" + "github.com/tinylib/msgp/msgp" ) func TestNewEvent(t *testing.T) { @@ -38,11 +39,29 @@ func TestEventToSpan(t *testing.T) { require.Len(t, spans, 1) require.Equal(t, "op", spans[0].OperationName()) - eventsMap := spans[0].Tag("_dd.stack").(internal.MetaStructValue).Value.(map[EventCategory][]*Event) - require.Len(t, eventsMap, 3) + eventsMap := spans[0].Tag("_dd.stack").(internal.MetaStructValue).Value.(map[string]any) + require.Len(t, eventsMap, 1) - eventsCat := eventsMap[ExceptionEvent] + eventsCat := eventsMap[string(ExceptionEvent)].([]*Event) require.Len(t, eventsCat, 1) require.Equal(t, *event, *eventsCat[0]) } + +func TestMsgPackSerialization(t *testing.T) { + mt := mocktracer.Start() + defer mt.Stop() + + span := ddtracer.StartSpan("op") + event := NewEvent(ExceptionEvent, WithMessage("message"), WithType("type"), WithID("id")) + AddToSpan(span, event) + span.Finish() + + spans := mt.FinishedSpans() + require.Len(t, spans, 1) + + eventsMap := spans[0].Tag("_dd.stack").(internal.MetaStructValue).Value + + _, err := msgp.AppendIntf(nil, eventsMap) + require.NoError(t, err) +}