diff --git a/CODEOWNERS b/CODEOWNERS
index 3328c09d71..b0c28372e5 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -14,3 +14,4 @@
# appsec
/internal/appsec @DataDog/appsec-go
+/contrib/**/appsec.go @DataDog/appsec-go
diff --git a/contrib/go-chi/chi.v5/chi.go b/contrib/go-chi/chi.v5/chi.go
index 9a5cae94d2..477f8e9636 100644
--- a/contrib/go-chi/chi.v5/chi.go
+++ b/contrib/go-chi/chi.v5/chi.go
@@ -31,6 +31,10 @@ func Middleware(opts ...Option) func(next http.Handler) http.Handler {
log.Debug("contrib/go-chi/chi.v5: Configuring Middleware: %#v", cfg)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if cfg.ignoreRequest(r) {
+ next.ServeHTTP(w, r)
+ return
+ }
opts := []ddtrace.StartSpanOption{
tracer.SpanType(ext.SpanTypeWeb),
tracer.ServiceName(cfg.serviceName),
diff --git a/contrib/go-chi/chi.v5/chi_test.go b/contrib/go-chi/chi.v5/chi_test.go
index 1c90f06066..3cd1c3294e 100644
--- a/contrib/go-chi/chi.v5/chi_test.go
+++ b/contrib/go-chi/chi.v5/chi_test.go
@@ -10,6 +10,7 @@ import (
"net/http"
"net/http/httptest"
"strconv"
+ "strings"
"testing"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
@@ -270,3 +271,33 @@ func TestAnalyticsSettings(t *testing.T) {
assertRate(t, mt, 0.23, WithAnalyticsRate(0.23))
})
}
+
+func TestIgnoreRequest(t *testing.T) {
+ router := chi.NewRouter()
+ router.Use(Middleware(
+ WithIgnoreRequest(func(r *http.Request) bool {
+ return strings.HasPrefix(r.URL.Path, "/skip")
+ }),
+ ))
+
+ router.Get("/ok", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("ok"))
+ })
+
+ router.Get("/skip", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("skip"))
+ })
+
+ for path, shouldSkip := range map[string]bool{
+ "/ok": false,
+ "/skip": true,
+ "/skipfoo": true,
+ } {
+ mt := mocktracer.Start()
+ defer mt.Reset()
+
+ r := httptest.NewRequest("GET", "http://localhost"+path, nil)
+ router.ServeHTTP(httptest.NewRecorder(), r)
+ assert.Equal(t, shouldSkip, len(mt.FinishedSpans()) == 0)
+ }
+}
diff --git a/contrib/go-chi/chi.v5/option.go b/contrib/go-chi/chi.v5/option.go
index f9a6b2aaa4..6c19dd3cc7 100644
--- a/contrib/go-chi/chi.v5/option.go
+++ b/contrib/go-chi/chi.v5/option.go
@@ -7,6 +7,7 @@ package chi
import (
"math"
+ "net/http"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
@@ -18,6 +19,7 @@ type config struct {
spanOpts []ddtrace.StartSpanOption // additional span options to be applied
analyticsRate float64
isStatusError func(statusCode int) bool
+ ignoreRequest func(r *http.Request) bool
}
// Option represents an option that can be passed to NewRouter.
@@ -34,6 +36,7 @@ func defaults(cfg *config) {
cfg.analyticsRate = globalconfig.AnalyticsRate()
}
cfg.isStatusError = isServerError
+ cfg.ignoreRequest = func(_ *http.Request) bool { return false }
}
// WithServiceName sets the given service name for the router.
@@ -85,3 +88,11 @@ func WithStatusCheck(fn func(statusCode int) bool) Option {
func isServerError(statusCode int) bool {
return statusCode >= 500 && statusCode < 600
}
+
+// WithIgnoreRequest specifies a function to use for determining if the
+// incoming HTTP request tracing should be skipped.
+func WithIgnoreRequest(fn func(r *http.Request) bool) Option {
+ return func(cfg *config) {
+ cfg.ignoreRequest = fn
+ }
+}
diff --git a/contrib/go-chi/chi/chi.go b/contrib/go-chi/chi/chi.go
index e4a90a961e..c5866dd146 100644
--- a/contrib/go-chi/chi/chi.go
+++ b/contrib/go-chi/chi/chi.go
@@ -31,6 +31,10 @@ func Middleware(opts ...Option) func(next http.Handler) http.Handler {
log.Debug("contrib/go-chi/chi: Configuring Middleware: %#v", cfg)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if cfg.ignoreRequest(r) {
+ next.ServeHTTP(w, r)
+ return
+ }
opts := []ddtrace.StartSpanOption{
tracer.SpanType(ext.SpanTypeWeb),
tracer.ServiceName(cfg.serviceName),
diff --git a/contrib/go-chi/chi/chi_test.go b/contrib/go-chi/chi/chi_test.go
index c3972745e3..2fcf68977e 100644
--- a/contrib/go-chi/chi/chi_test.go
+++ b/contrib/go-chi/chi/chi_test.go
@@ -10,6 +10,7 @@ import (
"net/http"
"net/http/httptest"
"strconv"
+ "strings"
"testing"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
@@ -270,3 +271,33 @@ func TestAnalyticsSettings(t *testing.T) {
assertRate(t, mt, 0.23, WithAnalyticsRate(0.23))
})
}
+
+func TestIgnoreRequest(t *testing.T) {
+ router := chi.NewRouter()
+ router.Use(Middleware(
+ WithIgnoreRequest(func(r *http.Request) bool {
+ return strings.HasPrefix(r.URL.Path, "/skip")
+ }),
+ ))
+
+ router.Get("/ok", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("ok"))
+ })
+
+ router.Get("/skip", func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("skip"))
+ })
+
+ for path, shouldSkip := range map[string]bool{
+ "/ok": false,
+ "/skip": true,
+ "/skipfoo": true,
+ } {
+ mt := mocktracer.Start()
+ defer mt.Reset()
+
+ r := httptest.NewRequest("GET", "http://localhost"+path, nil)
+ router.ServeHTTP(httptest.NewRecorder(), r)
+ assert.Equal(t, shouldSkip, len(mt.FinishedSpans()) == 0)
+ }
+}
diff --git a/contrib/go-chi/chi/option.go b/contrib/go-chi/chi/option.go
index f9a6b2aaa4..6c19dd3cc7 100644
--- a/contrib/go-chi/chi/option.go
+++ b/contrib/go-chi/chi/option.go
@@ -7,6 +7,7 @@ package chi
import (
"math"
+ "net/http"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
@@ -18,6 +19,7 @@ type config struct {
spanOpts []ddtrace.StartSpanOption // additional span options to be applied
analyticsRate float64
isStatusError func(statusCode int) bool
+ ignoreRequest func(r *http.Request) bool
}
// Option represents an option that can be passed to NewRouter.
@@ -34,6 +36,7 @@ func defaults(cfg *config) {
cfg.analyticsRate = globalconfig.AnalyticsRate()
}
cfg.isStatusError = isServerError
+ cfg.ignoreRequest = func(_ *http.Request) bool { return false }
}
// WithServiceName sets the given service name for the router.
@@ -85,3 +88,11 @@ func WithStatusCheck(fn func(statusCode int) bool) Option {
func isServerError(statusCode int) bool {
return statusCode >= 500 && statusCode < 600
}
+
+// WithIgnoreRequest specifies a function to use for determining if the
+// incoming HTTP request tracing should be skipped.
+func WithIgnoreRequest(fn func(r *http.Request) bool) Option {
+ return func(cfg *config) {
+ cfg.ignoreRequest = fn
+ }
+}
diff --git a/contrib/google.golang.org/grpc/appsec.go b/contrib/google.golang.org/grpc/appsec.go
new file mode 100644
index 0000000000..cec9c2d424
--- /dev/null
+++ b/contrib/google.golang.org/grpc/appsec.go
@@ -0,0 +1,78 @@
+// 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 grpc
+
+import (
+ "encoding/json"
+ "net"
+
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/grpcsec"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
+
+ "golang.org/x/net/context"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/peer"
+)
+
+// UnaryHandler wrapper to use when AppSec is enabled to monitor its execution.
+func appsecUnaryHandlerMiddleware(span ddtrace.Span, handler grpc.UnaryHandler) grpc.UnaryHandler {
+ httpsec.SetAppSecTags(span)
+ return func(ctx context.Context, req interface{}) (interface{}, error) {
+ op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{}, nil)
+ defer func() {
+ events := op.Finish(grpcsec.HandlerOperationRes{})
+ if len(events) == 0 {
+ return
+ }
+ setAppSecTags(ctx, span, events)
+ }()
+ defer grpcsec.StartReceiveOperation(grpcsec.ReceiveOperationArgs{}, op).Finish(grpcsec.ReceiveOperationRes{Message: req})
+ return handler(ctx, req)
+ }
+}
+
+// StreamHandler wrapper to use when AppSec is enabled to monitor its execution.
+func appsecStreamHandlerMiddleware(span ddtrace.Span, handler grpc.StreamHandler) grpc.StreamHandler {
+ httpsec.SetAppSecTags(span)
+ return func(srv interface{}, stream grpc.ServerStream) error {
+ op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{}, nil)
+ defer func() {
+ events := op.Finish(grpcsec.HandlerOperationRes{})
+ if len(events) == 0 {
+ return
+ }
+ setAppSecTags(stream.Context(), span, events)
+ }()
+ return handler(srv, appsecServerStream{ServerStream: stream, handlerOperation: op})
+ }
+}
+
+type appsecServerStream struct {
+ grpc.ServerStream
+ handlerOperation *grpcsec.HandlerOperation
+}
+
+// RecvMsg implements grpc.ServerStream interface method to monitor its
+// execution with AppSec.
+func (ss appsecServerStream) RecvMsg(m interface{}) error {
+ op := grpcsec.StartReceiveOperation(grpcsec.ReceiveOperationArgs{}, ss.handlerOperation)
+ defer func() {
+ op.Finish(grpcsec.ReceiveOperationRes{Message: m})
+ }()
+ return ss.ServerStream.RecvMsg(m)
+}
+
+// Set the AppSec tags when security events were found.
+func setAppSecTags(ctx context.Context, span ddtrace.Span, events []json.RawMessage) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ var addr net.Addr
+ if p, ok := peer.FromContext(ctx); ok {
+ addr = p.Addr
+ }
+ grpcsec.SetSecurityEventTags(span, events, addr, md)
+}
diff --git a/contrib/google.golang.org/grpc/appsec_test.go b/contrib/google.golang.org/grpc/appsec_test.go
new file mode 100644
index 0000000000..f424380cb4
--- /dev/null
+++ b/contrib/google.golang.org/grpc/appsec_test.go
@@ -0,0 +1,91 @@
+// 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 grpc
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
+)
+
+func TestAppSec(t *testing.T) {
+ appsec.Start()
+ defer appsec.Stop()
+ if !appsec.Enabled() {
+ t.Skip("appsec disabled")
+ }
+
+ rig, err := newRig(false)
+ require.NoError(t, err)
+ defer rig.Close()
+
+ client := rig.client
+
+ t.Run("unary", func(t *testing.T) {
+ mt := mocktracer.Start()
+ defer mt.Stop()
+
+ // Send a XSS attack
+ res, err := client.Ping(context.Background(), &FixtureRequest{Name: ""})
+ // Check that the handler was properly called
+ require.NoError(t, err)
+ require.Equal(t, "passed", res.Message)
+
+ finished := mt.FinishedSpans()
+ require.Len(t, finished, 1)
+
+ // The request should have the XSS attack attempt event (appsec rule id crs-941-100).
+ event := finished[0].Tag("_dd.appsec.json")
+ require.NotNil(t, event)
+ require.True(t, strings.Contains(event.(string), "crs-941-100"))
+ })
+
+ t.Run("stream", func(t *testing.T) {
+ mt := mocktracer.Start()
+ defer mt.Stop()
+
+ stream, err := client.StreamPing(context.Background())
+ require.NoError(t, err)
+
+ // Send a XSS attack
+ err = stream.Send(&FixtureRequest{Name: ""})
+ require.NoError(t, err)
+
+ // Check that the handler was properly called
+ res, err := stream.Recv()
+ require.Equal(t, "passed", res.Message)
+ require.NoError(t, err)
+
+ // Send a SQLi attack
+ err = stream.Send(&FixtureRequest{Name: "something UNION SELECT * from users"})
+ require.NoError(t, err)
+
+ // Check that the handler was properly called
+ res, err = stream.Recv()
+ require.Equal(t, "passed", res.Message)
+ require.NoError(t, err)
+
+ err = stream.CloseSend()
+ require.NoError(t, err)
+ // to flush the spans
+ stream.Recv()
+
+ finished := mt.FinishedSpans()
+ require.Len(t, finished, 6)
+
+ // The request should both attacks: the XSS and SQLi attack attempt
+ // events (appsec rule id crs-941-100, crs-942-100).
+ event := finished[5].Tag("_dd.appsec.json")
+ require.NotNil(t, event)
+ require.True(t, strings.Contains(event.(string), "crs-941-100"))
+ require.True(t, strings.Contains(event.(string), "crs-942-100"))
+ })
+}
diff --git a/contrib/google.golang.org/grpc/server.go b/contrib/google.golang.org/grpc/server.go
index 11320294bc..89806f2ff5 100644
--- a/contrib/google.golang.org/grpc/server.go
+++ b/contrib/google.golang.org/grpc/server.go
@@ -8,11 +8,12 @@ package grpc
import (
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"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/log"
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
- context "golang.org/x/net/context"
+ "golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
@@ -101,18 +102,19 @@ func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor {
span.SetTag(tagMethodKind, methodKindClientStream)
}
defer func() { finishWithError(span, err, cfg) }()
+ if appsec.Enabled() {
+ handler = appsecStreamHandlerMiddleware(span, handler)
+ }
}
// call the original handler with a new stream, which traces each send
// and recv if message tracing is enabled
- err = handler(srv, &serverStream{
+ return handler(srv, &serverStream{
ServerStream: ss,
cfg: cfg,
method: info.FullMethod,
ctx: ctx,
})
-
- return err
}
}
@@ -154,6 +156,9 @@ func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
}
}
}
+ if appsec.Enabled() {
+ handler = appsecUnaryHandlerMiddleware(span, handler)
+ }
resp, err := handler(ctx, req)
finishWithError(span, err, cfg)
return resp, err
diff --git a/contrib/google.golang.org/grpc/stats_server_test.go b/contrib/google.golang.org/grpc/stats_server_test.go
index 389fd1b312..a35efb1c0e 100644
--- a/contrib/google.golang.org/grpc/stats_server_test.go
+++ b/contrib/google.golang.org/grpc/stats_server_test.go
@@ -11,7 +11,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- context "golang.org/x/net/context"
+ "golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/stats"
diff --git a/contrib/net/http/http.go b/contrib/net/http/http.go
index a98821b94a..7be8c5bf80 100644
--- a/contrib/net/http/http.go
+++ b/contrib/net/http/http.go
@@ -64,6 +64,10 @@ func WrapHandler(h http.Handler, service, resource string, opts ...Option) http.
}
log.Debug("contrib/net/http: Wrapping Handler: Service: %s, Resource: %s, %#v", service, resource, cfg)
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ if cfg.ignoreRequest(req) {
+ h.ServeHTTP(w, req)
+ return
+ }
httputil.TraceAndServe(h, &httputil.TraceConfig{
ResponseWriter: w,
Request: req,
diff --git a/contrib/net/http/http_test.go b/contrib/net/http/http_test.go
index 9cdf5b511b..4041da2276 100644
--- a/contrib/net/http/http_test.go
+++ b/contrib/net/http/http_test.go
@@ -216,14 +216,15 @@ func TestIgnoreRequestOption(t *testing.T) {
spanCount: 1,
},
}
- mux := NewServeMux(WithIgnoreRequest(func(req *http.Request) bool {
+ ignore := func(req *http.Request) bool {
return req.URL.Path == "/skip"
- }))
+ }
+ mux := NewServeMux(WithIgnoreRequest(ignore))
mux.HandleFunc("/skip", handler200)
mux.HandleFunc("/200", handler200)
for _, test := range tests {
- t.Run(test.url, func(t *testing.T) {
+ t.Run("servemux"+test.url, func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
r := httptest.NewRequest("GET", "http://localhost"+test.url, nil)
@@ -233,6 +234,19 @@ func TestIgnoreRequestOption(t *testing.T) {
spans := mt.FinishedSpans()
assert.Equal(t, test.spanCount, len(spans))
})
+
+ t.Run("wraphandler"+test.url, func(t *testing.T) {
+ mt := mocktracer.Start()
+ defer mt.Stop()
+ r := httptest.NewRequest("GET", "http://localhost"+test.url, nil)
+ w := httptest.NewRecorder()
+ f := http.HandlerFunc(handler200)
+ handler := WrapHandler(f, "my-service", "my-resource", WithIgnoreRequest(ignore))
+ handler.ServeHTTP(w, r)
+
+ spans := mt.FinishedSpans()
+ assert.Equal(t, test.spanCount, len(spans))
+ })
}
}
diff --git a/ddtrace/tracer/log.go b/ddtrace/tracer/log.go
index b0ca8efe13..f609c4b774 100644
--- a/ddtrace/tracer/log.go
+++ b/ddtrace/tracer/log.go
@@ -17,13 +17,10 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/osinfo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/version"
)
-const (
- unknown = "unknown"
-)
-
// startupInfo contains various information about the status of the tracer on startup.
type startupInfo struct {
Date string `json:"date"` // ISO 8601 date and time of start
@@ -82,8 +79,8 @@ func logStartup(t *tracer) {
info := startupInfo{
Date: time.Now().Format(time.RFC3339),
- OSName: osName(),
- OSVersion: osVersion(),
+ OSName: osinfo.OSName(),
+ OSVersion: osinfo.OSVersion(),
Version: version.Tag,
Lang: "Go",
LangVersion: runtime.Version(),
diff --git a/internal/appsec/dyngo/instrumentation/grpcsec/grpc.go b/internal/appsec/dyngo/instrumentation/grpcsec/grpc.go
new file mode 100644
index 0000000000..41fe5d3838
--- /dev/null
+++ b/internal/appsec/dyngo/instrumentation/grpcsec/grpc.go
@@ -0,0 +1,177 @@
+// 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 grpcsec is the gRPC instrumentation API and contract for AppSec
+// defining an abstract run-time representation of gRPC handlers.
+// gRPC integrations must use this package to enable AppSec features for gRPC,
+// which listens to this package's operation events.
+package grpcsec
+
+import (
+ "encoding/json"
+ "reflect"
+ "sync"
+
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
+)
+
+// Abstract gRPC server handler operation definitions. It is based on two
+// operations allowing to describe every type of RPC: the HandlerOperation type
+// which represents the RPC handler, and the ReceiveOperation type which
+// represents the messages the RPC handler receives during its lifetime.
+// This means that the ReceiveOperation(s) will happen within the
+// HandlerOperation.
+// Every type of RPC, unary, client streaming, server streaming, and
+// bidirectional streaming RPCs, can be all represented with a HandlerOperation
+// having one or several ReceiveOperation.
+// The send operation is not required for now and therefore not defined, which
+// means that server and bidirectional streaming RPCs currently have the same
+// run-time representation as unary and client streaming RPCs.
+type (
+ // HandlerOperation represents a gRPC server handler operation.
+ // It must be created with StartHandlerOperation() and finished with its
+ // Finish() method.
+ // Security events observed during the operation lifetime should be added
+ // to the operation using its AddSecurityEvent() method.
+ HandlerOperation struct {
+ dyngo.Operation
+
+ events []json.RawMessage
+ mu sync.Mutex
+ }
+ // HandlerOperationArgs is the grpc handler arguments. Empty as of today.
+ HandlerOperationArgs struct{}
+ // HandlerOperationRes is the grpc handler results. Empty as of today.
+ HandlerOperationRes struct{}
+
+ // ReceiveOperation type representing an gRPC server handler operation. It must
+ // be created with StartReceiveOperation() and finished with its Finish().
+ ReceiveOperation struct {
+ dyngo.Operation
+ }
+ // ReceiveOperationArgs is the gRPC handler receive operation arguments
+ // Empty as of today.
+ ReceiveOperationArgs struct{}
+ // ReceiveOperationRes is the gRPC handler receive operation results which
+ // contains the message the gRPC handler received.
+ ReceiveOperationRes struct {
+ // Message received by the gRPC handler.
+ // Corresponds to the address `grpc.server.request.message`.
+ Message interface{}
+ }
+)
+
+// TODO(Julio-Guerra): create a go-generate tool to generate the types, vars and methods below
+
+// StartHandlerOperation starts an gRPC server handler operation, along with the
+// given arguments and parent operation, and emits a start event up in the
+// operation stack. When parent is nil, the operation is linked to the global
+// root operation.
+func StartHandlerOperation(args HandlerOperationArgs, parent dyngo.Operation) *HandlerOperation {
+ op := &HandlerOperation{Operation: dyngo.NewOperation(parent)}
+ dyngo.StartOperation(op, args)
+ return op
+}
+
+// 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) []json.RawMessage {
+ dyngo.FinishOperation(op, res)
+ return op.events
+}
+
+// AddSecurityEvent adds the security event to the list of events observed
+// during the operation lifetime.
+func (op *HandlerOperation) AddSecurityEvent(event json.RawMessage) {
+ op.mu.Lock()
+ defer op.mu.Unlock()
+ op.events = append(op.events, event)
+}
+
+// gRPC handler operation's start and finish event callback function types.
+type (
+ // OnHandlerOperationStart function type, called when an gRPC handler
+ // operation starts.
+ OnHandlerOperationStart func(*HandlerOperation, HandlerOperationArgs)
+ // OnHandlerOperationFinish function type, called when an gRPC handler
+ // operation finishes.
+ OnHandlerOperationFinish func(*HandlerOperation, HandlerOperationRes)
+)
+
+var (
+ handlerOperationArgsType = reflect.TypeOf((*HandlerOperationArgs)(nil)).Elem()
+ handlerOperationResType = reflect.TypeOf((*HandlerOperationRes)(nil)).Elem()
+)
+
+// ListenedType returns the type a OnHandlerOperationStart event listener
+// listens to, which is the HandlerOperationArgs type.
+func (OnHandlerOperationStart) ListenedType() reflect.Type { return handlerOperationArgsType }
+
+// Call the underlying event listener function by performing the type-assertion
+// on v whose type is the one returned by ListenedType().
+func (f OnHandlerOperationStart) Call(op dyngo.Operation, v interface{}) {
+ f(op.(*HandlerOperation), v.(HandlerOperationArgs))
+}
+
+// ListenedType returns the type a OnHandlerOperationFinish event listener
+// listens to, which is the HandlerOperationRes type.
+func (OnHandlerOperationFinish) ListenedType() reflect.Type { return handlerOperationResType }
+
+// Call the underlying event listener function by performing the type-assertion
+// on v whose type is the one returned by ListenedType().
+func (f OnHandlerOperationFinish) Call(op dyngo.Operation, v interface{}) {
+ f(op.(*HandlerOperation), v.(HandlerOperationRes))
+}
+
+// StartReceiveOperation starts a receive operation of a gRPC handler, along
+// with the given arguments and parent operation, and emits a start event up in
+// the operation stack. When parent is nil, the operation is linked to the
+// global root operation.
+func StartReceiveOperation(args ReceiveOperationArgs, parent dyngo.Operation) ReceiveOperation {
+ op := ReceiveOperation{Operation: dyngo.NewOperation(parent)}
+ dyngo.StartOperation(op, args)
+ return op
+}
+
+// Finish the gRPC handler operation, along with the given results, and emits a
+// finish event up in the operation stack.
+func (op ReceiveOperation) Finish(res ReceiveOperationRes) {
+ dyngo.FinishOperation(op, res)
+}
+
+// gRPC receive operation's start and finish event callback function types.
+type (
+ // OnReceiveOperationStart function type, called when a gRPC receive
+ // operation starts.
+ OnReceiveOperationStart func(ReceiveOperation, ReceiveOperationArgs)
+ // OnReceiveOperationFinish function type, called when a grpc receive
+ // operation finishes.
+ OnReceiveOperationFinish func(ReceiveOperation, ReceiveOperationRes)
+)
+
+var (
+ receiveOperationArgsType = reflect.TypeOf((*ReceiveOperationArgs)(nil)).Elem()
+ receiveOperationResType = reflect.TypeOf((*ReceiveOperationRes)(nil)).Elem()
+)
+
+// ListenedType returns the type a OnHandlerOperationStart event listener
+// listens to, which is the HandlerOperationArgs type.
+func (OnReceiveOperationStart) ListenedType() reflect.Type { return receiveOperationArgsType }
+
+// Call the underlying event listener function by performing the type-assertion
+// on v whose type is the one returned by ListenedType().
+func (f OnReceiveOperationStart) Call(op dyngo.Operation, v interface{}) {
+ f(op.(ReceiveOperation), v.(ReceiveOperationArgs))
+}
+
+// ListenedType returns the type a OnHandlerOperationFinish event listener
+// listens to, which is the HandlerOperationRes type.
+func (OnReceiveOperationFinish) ListenedType() reflect.Type { return receiveOperationResType }
+
+// Call the underlying event listener function by performing the type-assertion
+// on v whose type is the one returned by ListenedType().
+func (f OnReceiveOperationFinish) Call(op dyngo.Operation, v interface{}) {
+ f(op.(ReceiveOperation), v.(ReceiveOperationRes))
+}
diff --git a/internal/appsec/dyngo/instrumentation/grpcsec/grpc_test.go b/internal/appsec/dyngo/instrumentation/grpcsec/grpc_test.go
new file mode 100644
index 0000000000..cc7e637c1b
--- /dev/null
+++ b/internal/appsec/dyngo/instrumentation/grpcsec/grpc_test.go
@@ -0,0 +1,85 @@
+// 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 grpcsec_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/grpcsec"
+)
+
+func TestUsage(t *testing.T) {
+ testRPCRepresentation := func(expectedRecvOperation int) func(*testing.T) {
+ return func(t *testing.T) {
+ type (
+ rootArgs struct{}
+ rootRes struct{}
+ )
+ localRootOp := dyngo.NewOperation(nil)
+ dyngo.StartOperation(localRootOp, rootArgs{})
+ defer dyngo.FinishOperation(localRootOp, rootRes{})
+
+ var handlerStarted, handlerFinished, recvStarted, recvFinished int
+ defer func() {
+ require.Equal(t, 1, handlerStarted)
+ require.Equal(t, 1, handlerFinished)
+ require.Equal(t, expectedRecvOperation, recvStarted)
+ require.Equal(t, expectedRecvOperation, recvFinished)
+ }()
+
+ const expectedMessageFormat = "message number %d"
+
+ localRootOp.On(grpcsec.OnHandlerOperationStart(func(handlerOp *grpcsec.HandlerOperation, args grpcsec.HandlerOperationArgs) {
+ handlerStarted++
+
+ handlerOp.On(grpcsec.OnReceiveOperationStart(func(op grpcsec.ReceiveOperation, _ grpcsec.ReceiveOperationArgs) {
+ recvStarted++
+
+ op.On(grpcsec.OnReceiveOperationFinish(func(_ grpcsec.ReceiveOperation, res grpcsec.ReceiveOperationRes) {
+ expectedMessage := fmt.Sprintf(expectedMessageFormat, recvStarted)
+ require.Equal(t, expectedMessage, res.Message)
+ recvFinished++
+
+ handlerOp.AddSecurityEvent(json.RawMessage(expectedMessage))
+ }))
+ }))
+
+ handlerOp.On(grpcsec.OnHandlerOperationFinish(func(*grpcsec.HandlerOperation, grpcsec.HandlerOperationRes) {
+ handlerFinished++
+ }))
+ }))
+
+ rpcOp := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{}, localRootOp)
+
+ for i := 1; i <= expectedRecvOperation; i++ {
+ recvOp := grpcsec.StartReceiveOperation(grpcsec.ReceiveOperationArgs{}, rpcOp)
+ recvOp.Finish(grpcsec.ReceiveOperationRes{Message: fmt.Sprintf(expectedMessageFormat, i)})
+ }
+
+ secEvents := rpcOp.Finish(grpcsec.HandlerOperationRes{})
+
+ require.Len(t, secEvents, expectedRecvOperation)
+ for i, e := range secEvents {
+ require.Equal(t, fmt.Sprintf(expectedMessageFormat, i+1), string(e))
+ }
+ }
+ }
+
+ // Unary RPCs are represented by a single receive operation
+ t.Run("unary-representation", testRPCRepresentation(1))
+ // Client streaming RPCs are represented by many receive operations.
+ t.Run("client-streaming-representation", testRPCRepresentation(10))
+ // Server and bidirectional streaming RPCs cannot be tested for now because
+ // the send operations are not used nor defined yet, server streaming RPCs
+ // are currently represented like unary RPCs (1 client message, N server
+ // messages), and bidirectional RPCs like client streaming RPCs (N client
+ // messages, M server messages).
+}
diff --git a/internal/appsec/dyngo/instrumentation/grpcsec/tags.go b/internal/appsec/dyngo/instrumentation/grpcsec/tags.go
new file mode 100644
index 0000000000..9f36ce2fd1
--- /dev/null
+++ b/internal/appsec/dyngo/instrumentation/grpcsec/tags.go
@@ -0,0 +1,102 @@
+// 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 grpcsec
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/log"
+)
+
+// SetSecurityEventTags sets the AppSec-specific span tags when a security event
+// occurred into the service entry span.
+func SetSecurityEventTags(span ddtrace.Span, events []json.RawMessage, addr net.Addr, md map[string][]string) {
+ if err := setSecurityEventTags(span, events, addr, md); err != nil {
+ log.Error("appsec: %v", err)
+ }
+}
+
+func setSecurityEventTags(span ddtrace.Span, events []json.RawMessage, addr net.Addr, md map[string][]string) error {
+ if err := setEventSpanTags(span, events); err != nil {
+ return err
+ }
+ var ip string
+ switch actual := addr.(type) {
+ case *net.UDPAddr:
+ ip = actual.IP.String()
+ case *net.TCPAddr:
+ ip = actual.IP.String()
+ }
+ if ip != "" {
+ span.SetTag("network.client.ip", ip)
+ }
+ for h, v := range httpsec.NormalizeHTTPHeaders(md) {
+ span.SetTag("grpc.metadata."+h, v)
+ }
+ return nil
+}
+
+// setEventSpanTags sets the security event span tags into the service entry span.
+func setEventSpanTags(span ddtrace.Span, events []json.RawMessage) error {
+ // Set the appsec event span tag
+ val, err := makeEventTagValue(events)
+ if err != nil {
+ return err
+ }
+ span.SetTag("_dd.appsec.json", string(val))
+ // Keep this span due to the security event
+ span.SetTag(ext.ManualKeep, true)
+ span.SetTag("_dd.origin", "appsec")
+ // Set the appsec.event tag needed by the appsec backend
+ span.SetTag("appsec.event", true)
+ return nil
+}
+
+// Create the value of the security event tag.
+// TODO(Julio-Guerra): a future libddwaf version should return something
+// avoiding us the following events concatenation logic which currently
+// involves unserializing the top-level JSON arrays to concatenate them
+// together.
+// TODO(Julio-Guerra): avoid serializing the json in the request hot path
+func makeEventTagValue(events []json.RawMessage) (json.RawMessage, error) {
+ var v interface{}
+ if l := len(events); l == 1 {
+ // eventTag is the structure to use in the `_dd.appsec.json` span tag.
+ // In this case of 1 event, it already is an array as expected.
+ type eventTag struct {
+ Triggers json.RawMessage `json:"triggers"`
+ }
+ v = eventTag{Triggers: events[0]}
+ } else {
+ // eventTag is the structure to use in the `_dd.appsec.json` span tag.
+ // With more than one event, we need to concatenate the arrays together
+ // (ie. convert [][]json.RawMessage into []json.RawMessage).
+ type eventTag struct {
+ Triggers []json.RawMessage `json:"triggers"`
+ }
+ concatenated := make([]json.RawMessage, 0, l) // at least len(events)
+ for _, event := range events {
+ // Unmarshal the top level array
+ var tmp []json.RawMessage
+ if err := json.Unmarshal(event, &tmp); err != nil {
+ return nil, fmt.Errorf("unexpected error while unserializing the appsec event `%s`: %v", string(event), err)
+ }
+ concatenated = append(concatenated, tmp...)
+ }
+ v = eventTag{Triggers: concatenated}
+ }
+
+ tag, err := json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf("unexpected error while serializing the appsec event span tag: %v", err)
+ }
+ return tag, nil
+}
diff --git a/internal/appsec/dyngo/instrumentation/grpcsec/tags_test.go b/internal/appsec/dyngo/instrumentation/grpcsec/tags_test.go
new file mode 100644
index 0000000000..ce6e79144e
--- /dev/null
+++ b/internal/appsec/dyngo/instrumentation/grpcsec/tags_test.go
@@ -0,0 +1,182 @@
+// 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 grpcsec
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
+)
+
+func TestSetSecurityEventTags(t *testing.T) {
+ for _, eventCase := range []struct {
+ name string
+ events []json.RawMessage
+ expectedTag string
+ expectedError bool
+ }{
+ {
+ name: "one-event",
+ events: []json.RawMessage{json.RawMessage(`["one","two"]`)},
+ expectedTag: `{"triggers":["one","two"]}`,
+ },
+ {
+ name: "one-event-with-json-error",
+ events: []json.RawMessage{json.RawMessage(`["one",two"]`)},
+ expectedError: true,
+ },
+ {
+ name: "two-events",
+ events: []json.RawMessage{json.RawMessage(`["one","two"]`), json.RawMessage(`["three","four"]`)},
+ expectedTag: `{"triggers":["one","two","three","four"]}`,
+ },
+ {
+ name: "two-events-with-json-error",
+ events: []json.RawMessage{json.RawMessage(`["one","two"]`), json.RawMessage(`["three,"four"]`)},
+ expectedError: true,
+ },
+ {
+ name: "three-events-with-json-error",
+ events: []json.RawMessage{json.RawMessage(`["one","two"]`), json.RawMessage(`["three","four"]`), json.RawMessage(`"five"`)},
+ expectedError: true,
+ },
+ } {
+ eventCase := eventCase
+ for _, addrCase := range []struct {
+ name string
+ addr net.Addr
+ expectedTag string
+ }{
+ {
+ name: "tcp-ipv4-address",
+ addr: &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 6789},
+ expectedTag: "1.2.3.4",
+ },
+ {
+ name: "tcp-ipv6-address",
+ addr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 6789},
+ expectedTag: "::1",
+ },
+ {
+ name: "udp-ipv4-address",
+ addr: &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 6789},
+ expectedTag: "1.2.3.4",
+ },
+ {
+ name: "udp-ipv6-address",
+ addr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: 6789},
+ expectedTag: "::1",
+ },
+ {
+ name: "unix-socket-address",
+ addr: &net.UnixAddr{Name: "/var/my.sock"},
+ },
+ } {
+ addrCase := addrCase
+ for _, metadataCase := range []struct {
+ name string
+ md map[string][]string
+ expectedTags map[string]string
+ }{
+ {
+ name: "zero-metadata",
+ },
+ {
+ name: "xff-metadata",
+ md: map[string][]string{
+ "x-forwarded-for": {"1.2.3.4", "4.5.6.7"},
+ ":authority": {"something"},
+ },
+ expectedTags: map[string]string{
+ "grpc.metadata.x-forwarded-for": "1.2.3.4,4.5.6.7",
+ },
+ },
+ {
+ name: "xff-metadata",
+ md: map[string][]string{
+ "x-forwarded-for": {"1.2.3.4"},
+ ":authority": {"something"},
+ },
+ expectedTags: map[string]string{
+ "grpc.metadata.x-forwarded-for": "1.2.3.4",
+ },
+ },
+ {
+ name: "no-monitored-metadata",
+ md: map[string][]string{
+ ":authority": {"something"},
+ },
+ },
+ } {
+ metadataCase := metadataCase
+ t.Run(fmt.Sprintf("%s-%s-%s", eventCase.name, addrCase.name, metadataCase.name), func(t *testing.T) {
+ var span MockSpan
+ err := setSecurityEventTags(&span, eventCase.events, addrCase.addr, metadataCase.md)
+ if eventCase.expectedError {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+
+ expectedTags := map[string]interface{}{
+ "_dd.appsec.json": eventCase.expectedTag,
+ "manual.keep": true,
+ "appsec.event": true,
+ "_dd.origin": "appsec",
+ }
+
+ if addr := addrCase.expectedTag; addr != "" {
+ expectedTags["network.client.ip"] = addr
+ }
+
+ for k, v := range metadataCase.expectedTags {
+ expectedTags[k] = v
+ }
+
+ require.Equal(t, expectedTags, span.tags)
+ require.False(t, span.finished)
+ })
+ }
+ }
+ }
+}
+
+type MockSpan struct {
+ tags map[string]interface{}
+ finished bool
+}
+
+func (m *MockSpan) SetTag(key string, value interface{}) {
+ if m.tags == nil {
+ m.tags = make(map[string]interface{})
+ }
+ m.tags[key] = value
+}
+
+func (m *MockSpan) SetOperationName(operationName string) {
+ panic("unused")
+}
+
+func (m *MockSpan) BaggageItem(key string) string {
+ panic("unused")
+}
+
+func (m *MockSpan) SetBaggageItem(key, val string) {
+ panic("unused")
+}
+
+func (m *MockSpan) Finish(opts ...ddtrace.FinishOption) {
+ m.finished = true
+}
+
+func (m *MockSpan) Context() ddtrace.SpanContext {
+ panic("unused")
+}
diff --git a/internal/appsec/dyngo/instrumentation/httpsec/tags.go b/internal/appsec/dyngo/instrumentation/httpsec/tags.go
index 50de3a435a..77ae050586 100644
--- a/internal/appsec/dyngo/instrumentation/httpsec/tags.go
+++ b/internal/appsec/dyngo/instrumentation/httpsec/tags.go
@@ -47,10 +47,10 @@ func setEventSpanTags(span ddtrace.Span, events json.RawMessage) {
func SetSecurityEventTags(span ddtrace.Span, events json.RawMessage, remoteIP string, headers, respHeaders map[string][]string) {
setEventSpanTags(span, events)
span.SetTag("network.client.ip", remoteIP)
- for h, v := range normalizeHTTPHeaders(headers) {
+ for h, v := range NormalizeHTTPHeaders(headers) {
span.SetTag("http.request.headers."+h, v)
}
- for h, v := range normalizeHTTPHeaders(respHeaders) {
+ for h, v := range NormalizeHTTPHeaders(respHeaders) {
span.SetTag("http.response.headers."+h, v)
}
}
@@ -83,8 +83,9 @@ func init() {
sort.Strings(collectedHTTPHeaders[:])
}
-// normalizeHTTPHeaders returns the HTTP headers following Datadog's normalization format.
-func normalizeHTTPHeaders(headers map[string][]string) (normalized map[string]string) {
+// NormalizeHTTPHeaders returns the HTTP headers following Datadog's
+// normalization format.
+func NormalizeHTTPHeaders(headers map[string][]string) (normalized map[string]string) {
if len(headers) == 0 {
return nil
}
diff --git a/internal/appsec/dyngo/instrumentation/httpsec/tags_test.go b/internal/appsec/dyngo/instrumentation/httpsec/tags_test.go
index 591a433ab1..fc07db4c12 100644
--- a/internal/appsec/dyngo/instrumentation/httpsec/tags_test.go
+++ b/internal/appsec/dyngo/instrumentation/httpsec/tags_test.go
@@ -45,7 +45,7 @@ func TestNormalizeHTTPHeaders(t *testing.T) {
},
},
} {
- headers := normalizeHTTPHeaders(tc.headers)
+ headers := NormalizeHTTPHeaders(tc.headers)
require.Equal(t, tc.expected, headers)
}
}
diff --git a/internal/appsec/waf.go b/internal/appsec/waf.go
index 7bb8ef4280..4fd45d9329 100644
--- a/internal/appsec/waf.go
+++ b/internal/appsec/waf.go
@@ -12,9 +12,12 @@ import (
"errors"
"fmt"
"sort"
+ "sync"
+ "sync/atomic"
"time"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/grpcsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/waf"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
@@ -48,24 +51,38 @@ func registerWAF(rules []byte, timeout time.Duration) (unreg dyngo.UnregisterFun
return nil, errors.New("no addresses found in the rule")
}
// Check there are supported addresses in the rule
- addresses, notSupported := supportedAddresses(ruleAddresses)
- if len(addresses) == 0 {
+ httpAddresses, grpcAddresses, notSupported := supportedAddresses(ruleAddresses)
+ if len(httpAddresses) == 0 && len(grpcAddresses) == 0 {
return nil, fmt.Errorf("the addresses present in the rule are not supported: %v", notSupported)
} else if len(notSupported) > 0 {
- log.Debug("appsec: the addresses present in the rule are partially supported: supported=%v, not supported=%v", addresses, notSupported)
+ log.Debug("appsec: the addresses present in the rule are partially supported: not supported=%v", notSupported)
}
// Register the WAF event listener
- unregister := dyngo.Register(newWAFEventListener(waf, addresses, timeout))
+ var unregisterHTTP, unregisterGRPC dyngo.UnregisterFunc
+ if len(httpAddresses) > 0 {
+ log.Debug("appsec: registering http waf listening to addresses %v", httpAddresses)
+ unregisterHTTP = dyngo.Register(newHTTPWAFEventListener(waf, httpAddresses, timeout))
+ }
+ if len(grpcAddresses) > 0 {
+ log.Debug("appsec: registering grpc waf listening to addresses %v", grpcAddresses)
+ unregisterGRPC = dyngo.Register(newGRPCWAFEventListener(waf, grpcAddresses, timeout))
+ }
+
// Return an unregistration function that will also release the WAF instance.
return func() {
defer waf.Close()
- unregister()
+ if unregisterHTTP != nil {
+ unregisterHTTP()
+ }
+ if unregisterGRPC != nil {
+ unregisterGRPC()
+ }
}, nil
}
// newWAFEventListener returns the WAF event listener to register in order to enable it.
-func newWAFEventListener(handle *waf.Handle, addresses []string, timeout time.Duration) dyngo.EventListener {
+func newHTTPWAFEventListener(handle *waf.Handle, addresses []string, timeout time.Duration) dyngo.EventListener {
return httpsec.OnHandlerOperationStart(func(op *httpsec.Operation, args httpsec.HandlerOperationArgs) {
// At the moment, AppSec doesn't block the requests, and so we can use the fact we are in monitoring-only mode
// to call the WAF only once at the end of the handler operation.
@@ -106,6 +123,53 @@ func newWAFEventListener(handle *waf.Handle, addresses []string, timeout time.Du
})
}
+// newGRPCWAFEventListener returns the WAF event listener to register in order
+// to enable it.
+func newGRPCWAFEventListener(handle *waf.Handle, addresses []string, timeout time.Duration) dyngo.EventListener {
+ return grpcsec.OnHandlerOperationStart(func(op *grpcsec.HandlerOperation, _ grpcsec.HandlerOperationArgs) {
+ // Limit the maximum number of security events, as a streaming RPC could
+ // receive unlimited number of messages where we could find security events
+ const maxWAFEventsPerRequest = 10
+ var (
+ nbEvents uint32
+ logOnce sync.Once
+ )
+ op.On(grpcsec.OnReceiveOperationFinish(func(_ grpcsec.ReceiveOperation, res grpcsec.ReceiveOperationRes) {
+ if atomic.LoadUint32(&nbEvents) == maxWAFEventsPerRequest {
+ logOnce.Do(func() {
+ log.Debug("appsec: ignoring the rpc message due to the maximum number of security events per grpc call reached")
+ })
+ return
+ }
+ // The current workaround of the WAF context limitations is to
+ // simply instantiate and release the WAF context for the operation
+ // lifetime so that:
+ // 1. We avoid growing the memory usage of the context every time
+ // a grpc.server.request.message value is added to it during
+ // the RPC lifetime.
+ // 2. We avoid the limitation of 1 event per attack type.
+ // TODO(Julio-Guerra): a future libddwaf API should solve this out.
+ wafCtx := waf.NewContext(handle)
+ if wafCtx == nil {
+ // The WAF event listener got concurrently released
+ return
+ }
+ defer wafCtx.Close()
+ // Run the WAF on the rule addresses available in the args
+ // Note that we don't check if the address is present in the rules
+ // as we only support one at the moment, so this callback cannot be
+ // set when the address is not present.
+ events := runWAF(wafCtx, map[string]interface{}{grpcServerRequestMessage: res.Message}, timeout)
+ if len(events) == 0 {
+ return
+ }
+ log.Debug("appsec: attack detected by the grpc waf")
+ atomic.AddUint32(&nbEvents, 1)
+ op.AddSecurityEvent(events)
+ }))
+ })
+}
+
func runWAF(wafCtx *waf.Context, values map[string]interface{}, timeout time.Duration) []byte {
matches, err := wafCtx.Run(values, timeout)
if err != nil {
@@ -119,7 +183,7 @@ func runWAF(wafCtx *waf.Context, values map[string]interface{}, timeout time.Dur
return matches
}
-// Rule addresses currently supported by the WAF
+// HTTP rule addresses currently supported by the WAF
const (
serverRequestRawURIAddr = "server.request.uri.raw"
serverRequestHeadersNoCookiesAddr = "server.request.headers.no_cookies"
@@ -128,8 +192,8 @@ const (
serverResponseStatusAddr = "server.response.status"
)
-// List of rule addresses currently supported by the WAF
-var supportedAddressesList = []string{
+// List of HTTP rule addresses currently supported by the WAF
+var httpAddresses = []string{
serverRequestRawURIAddr,
serverRequestHeadersNoCookiesAddr,
serverRequestCookiesAddr,
@@ -137,23 +201,34 @@ var supportedAddressesList = []string{
serverResponseStatusAddr,
}
+// gRPC rule addresses currently supported by the WAF
+const (
+ grpcServerRequestMessage = "grpc.server.request.message"
+)
+
+// List of gRPC rule addresses currently supported by the WAF
+var grpcAddresses = []string{
+ grpcServerRequestMessage,
+}
+
func init() {
- sort.Strings(supportedAddressesList)
+ // sort the address lists to avoid mistakes and use sort.SearchStrings()
+ sort.Strings(httpAddresses)
+ sort.Strings(grpcAddresses)
}
// supportedAddresses returns the list of addresses we actually support from the
// given rule addresses.
-func supportedAddresses(ruleAddresses []string) (supported, notSupported []string) {
+func supportedAddresses(ruleAddresses []string) (supportedHTTP, supportedGRPC, notSupported []string) {
// Filter the supported addresses only
- l := len(supportedAddressesList)
- supported = make([]string, 0, l)
for _, addr := range ruleAddresses {
- if i := sort.SearchStrings(supportedAddressesList, addr); i < l && supportedAddressesList[i] == addr {
- supported = append(supported, addr)
+ if i := sort.SearchStrings(httpAddresses, addr); i < len(httpAddresses) && httpAddresses[i] == addr {
+ supportedHTTP = append(supportedHTTP, addr)
+ } else if i := sort.SearchStrings(grpcAddresses, addr); i < len(grpcAddresses) && grpcAddresses[i] == addr {
+ supportedGRPC = append(supportedGRPC, addr)
} else {
notSupported = append(notSupported, addr)
}
}
- // Check the resulting situation we are in
- return supported, notSupported
+ return
}
diff --git a/internal/osinfo/osinfo.go b/internal/osinfo/osinfo.go
new file mode 100644
index 0000000000..7519a917e9
--- /dev/null
+++ b/internal/osinfo/osinfo.go
@@ -0,0 +1,21 @@
+// 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 osinfo provides information about the current operating system release
+package osinfo
+
+// OSName returns the name of the operating system, including the distribution
+// for Linux when possible.
+func OSName() string {
+ // call out to OS-specific implementation
+ return osName()
+}
+
+// OSVersion returns the operating system release, e.g. major/minor version
+// number and build ID.
+func OSVersion() string {
+ // call out to OS-specific implementation
+ return osVersion()
+}
diff --git a/ddtrace/tracer/osinfo_darwin.go b/internal/osinfo/osinfo_darwin.go
similarity index 93%
rename from ddtrace/tracer/osinfo_darwin.go
rename to internal/osinfo/osinfo_darwin.go
index b8a68791ea..32ead5fe05 100644
--- a/ddtrace/tracer/osinfo_darwin.go
+++ b/internal/osinfo/osinfo_darwin.go
@@ -3,7 +3,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.
-package tracer
+package osinfo
import (
"os/exec"
@@ -18,7 +18,7 @@ func osName() string {
func osVersion() string {
out, err := exec.Command("sw_vers", "-productVersion").Output()
if err != nil {
- return unknown
+ return "unknown"
}
return strings.Trim(string(out), "\n")
}
diff --git a/ddtrace/tracer/osinfo_default.go b/internal/osinfo/osinfo_default.go
similarity index 81%
rename from ddtrace/tracer/osinfo_default.go
rename to internal/osinfo/osinfo_default.go
index c2937ebfb9..72d70d3885 100644
--- a/ddtrace/tracer/osinfo_default.go
+++ b/internal/osinfo/osinfo_default.go
@@ -3,9 +3,10 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.
+//go:build !windows && !linux && !darwin && !freebsd
// +build !windows,!linux,!darwin,!freebsd
-package tracer
+package osinfo
import (
"runtime"
@@ -16,5 +17,5 @@ func osName() string {
}
func osVersion() string {
- return unknown
+ return "unknown"
}
diff --git a/ddtrace/tracer/osinfo_freebsd.go b/internal/osinfo/osinfo_freebsd.go
similarity index 93%
rename from ddtrace/tracer/osinfo_freebsd.go
rename to internal/osinfo/osinfo_freebsd.go
index 03433890c2..543f2ffdfd 100644
--- a/ddtrace/tracer/osinfo_freebsd.go
+++ b/internal/osinfo/osinfo_freebsd.go
@@ -3,7 +3,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.
-package tracer
+package osinfo
import (
"os/exec"
@@ -18,7 +18,7 @@ func osName() string {
func osVersion() string {
out, err := exec.Command("uname", "-r").Output()
if err != nil {
- return unknown
+ return "unknown"
}
return strings.Split(string(out), "-")[0]
}
diff --git a/ddtrace/tracer/osinfo_linux.go b/internal/osinfo/osinfo_linux.go
similarity index 94%
rename from ddtrace/tracer/osinfo_linux.go
rename to internal/osinfo/osinfo_linux.go
index 85c09ab818..6196d4d12d 100644
--- a/ddtrace/tracer/osinfo_linux.go
+++ b/internal/osinfo/osinfo_linux.go
@@ -3,7 +3,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.
-package tracer
+package osinfo
import (
"bufio"
@@ -32,11 +32,11 @@ func osName() string {
func osVersion() string {
f, err := os.Open("/etc/os-release")
if err != nil {
- return unknown
+ return "unknown"
}
defer f.Close()
s := bufio.NewScanner(f)
- version := unknown
+ version := "unknown"
for s.Scan() {
parts := strings.SplitN(s.Text(), "=", 2)
switch parts[0] {
diff --git a/ddtrace/tracer/osinfo_windows.go b/internal/osinfo/osinfo_windows.go
similarity index 94%
rename from ddtrace/tracer/osinfo_windows.go
rename to internal/osinfo/osinfo_windows.go
index bffbd144ef..659bd9ce6a 100644
--- a/ddtrace/tracer/osinfo_windows.go
+++ b/internal/osinfo/osinfo_windows.go
@@ -3,7 +3,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016 Datadog, Inc.
-package tracer
+package osinfo
import (
"fmt"
@@ -20,7 +20,7 @@ func osName() string {
func osVersion() string {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
if err != nil {
- return unknown
+ return "unknown"
}
defer k.Close()
@@ -34,7 +34,7 @@ func osVersion() string {
version.WriteString(fmt.Sprintf(".%d", min))
}
} else {
- version.WriteString(unknown)
+ version.WriteString("unknown")
}
ed, _, err := k.GetStringValue("EditionID")
diff --git a/profiler/options.go b/profiler/options.go
index 24f773dc61..0fed66e0ba 100644
--- a/profiler/options.go
+++ b/profiler/options.go
@@ -7,6 +7,7 @@ package profiler
import (
"context"
+ "encoding/json"
"fmt"
"net"
"net/http"
@@ -22,6 +23,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
+ "gopkg.in/DataDog/dd-trace-go.v1/internal/osinfo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/version"
"github.com/DataDog/datadog-go/v5/statsd"
@@ -100,6 +102,62 @@ type config struct {
blockRate int
outputDir string
deltaProfiles bool
+ logStartup bool
+}
+
+// logStartup records the configuration to the configured logger in JSON format
+func logStartup(c *config) {
+ info := struct {
+ Date string `json:"date"` // ISO 8601 date and time of start
+ OSName string `json:"os_name"` // Windows, Darwin, Debian, etc.
+ OSVersion string `json:"os_version"` // Version of the OS
+ Version string `json:"version"` // Profiler version
+ Lang string `json:"lang"` // "Go"
+ LangVersion string `json:"lang_version"` // Go version, e.g. go1.13
+ Hostname string `json:"hostname"`
+ DeltaProfiles bool `json:"delta_profiles"`
+ Service string `json:"service"`
+ Env string `json:"env"`
+ TargetURL string `json:"target_url"`
+ Agentless bool `json:"agentless"`
+ Tags []string `json:"tags"`
+ ProfilePeriod string `json:"profile_period"`
+ EnabledProfiles []string `json:"enabled_profiles"`
+ CPUDuration string `json:"cpu_duration"`
+ BlockProfileRate int `json:"block_profile_rate"`
+ MutexProfileFraction int `json:"mutex_profile_fraction"`
+ MaxGoroutinesWait int `json:"max_goroutines_wait"`
+ UploadTimeout string `json:"upload_timeout"`
+ }{
+ Date: time.Now().Format(time.RFC3339),
+ OSName: osinfo.OSName(),
+ OSVersion: osinfo.OSVersion(),
+ Version: version.Tag,
+ Lang: "Go",
+ LangVersion: runtime.Version(),
+ Hostname: c.hostname,
+ DeltaProfiles: c.deltaProfiles,
+ Service: c.service,
+ Env: c.env,
+ TargetURL: c.targetURL,
+ Agentless: c.agentless,
+ Tags: c.tags,
+ ProfilePeriod: c.period.String(),
+ CPUDuration: c.cpuDuration.String(),
+ BlockProfileRate: c.blockRate,
+ MutexProfileFraction: c.mutexFraction,
+ MaxGoroutinesWait: c.maxGoroutinesWait,
+ UploadTimeout: c.uploadTimeout.String(),
+ }
+ for t := range c.types {
+ info.EnabledProfiles = append(info.EnabledProfiles, t.String())
+ }
+ b, err := json.Marshal(info)
+ if err != nil {
+ log.Error("Marshaling profiler configuration: %s", err)
+ return
+ }
+ log.Info("Profiler configuration: %s\n", b)
}
func urlForSite(site string) (string, error) {
@@ -143,6 +201,7 @@ func defaultConfig() (*config, error) {
maxGoroutinesWait: 1000, // arbitrary value, should limit STW to ~30ms
tags: []string{fmt.Sprintf("pid:%d", os.Getpid())},
deltaProfiles: internal.BoolEnv("DD_PROFILING_DELTA", true),
+ logStartup: true,
}
for _, t := range defaultProfileTypes {
c.addProfileType(t)
@@ -411,3 +470,12 @@ func withOutputDir(dir string) Option {
cfg.outputDir = dir
}
}
+
+// WithLogStartup toggles logging the configuration of the profiler to standard
+// error when profiling is started. The configuration is logged in a JSON
+// format. This option is enabled by default.
+func WithLogStartup(enabled bool) Option {
+ return func(cfg *config) {
+ cfg.logStartup = enabled
+ }
+}
diff --git a/profiler/profiler.go b/profiler/profiler.go
index cae7d0161a..ee9088ff35 100644
--- a/profiler/profiler.go
+++ b/profiler/profiler.go
@@ -131,6 +131,9 @@ func newProfiler(opts ...Option) (*profiler, error) {
return nil, fmt.Errorf("unknown profile type: %d", pt)
}
}
+ if cfg.logStartup {
+ logStartup(cfg)
+ }
p := profiler{
cfg: cfg,
diff --git a/profiler/profiler_test.go b/profiler/profiler_test.go
index 80232a82ab..1974784e27 100644
--- a/profiler/profiler_test.go
+++ b/profiler/profiler_test.go
@@ -10,6 +10,7 @@ import (
"net"
"os"
"runtime"
+ "strings"
"sync"
"sync/atomic"
"testing"
@@ -21,13 +22,31 @@ import (
"github.com/stretchr/testify/require"
)
+func TestMain(m *testing.M) {
+ // Profiling configuration is logged by default when starting a profile,
+ // so we want to discard it during tests to avoid flooding the terminal
+ // with logs
+ log.UseLogger(log.DiscardLogger{})
+ os.Exit(m.Run())
+}
+
func TestStart(t *testing.T) {
t.Run("defaults", func(t *testing.T) {
+ rl := &log.RecordLogger{}
+ defer log.UseLogger(rl)()
+
if err := Start(); err != nil {
t.Fatal(err)
}
defer Stop()
+ // Profiler configuration should be logged by default. Check
+ // that we log some default configuration, e.g. enabled profiles
+ assert.LessOrEqual(t, 1, len(rl.Logs()))
+ startupLog := strings.Join(rl.Logs(), " ")
+ assert.Contains(t, startupLog, "cpu")
+ assert.Contains(t, startupLog, "heap")
+
mu.Lock()
require.NotNil(t, activeProfiler)
assert := assert.New(t)
@@ -68,8 +87,10 @@ func TestStart(t *testing.T) {
defer Stop()
assert.Nil(t, err)
assert.Equal(t, activeProfiler.cfg.agentURL, activeProfiler.cfg.targetURL)
- assert.Equal(t, 1, len(rl.Logs()))
- assert.Contains(t, rl.Logs()[0], "profiler.WithAPIKey")
+ // The package should log a warning that using an API has no
+ // effect unless uploading directly to Datadog (i.e. agentless)
+ assert.LessOrEqual(t, 1, len(rl.Logs()))
+ assert.Contains(t, strings.Join(rl.Logs(), " "), "profiler.WithAPIKey")
})
t.Run("options/GoodAPIKey/Agentless", func(t *testing.T) {
@@ -83,8 +104,10 @@ func TestStart(t *testing.T) {
defer Stop()
assert.Nil(t, err)
assert.Equal(t, activeProfiler.cfg.apiURL, activeProfiler.cfg.targetURL)
- assert.Equal(t, 1, len(rl.Logs()))
- assert.Contains(t, rl.Logs()[0], "profiler.WithAgentlessUpload")
+ // The package should log a warning that agentless upload is not
+ // officially supported, so prefer not to use it
+ assert.LessOrEqual(t, 1, len(rl.Logs()))
+ assert.Contains(t, strings.Join(rl.Logs(), " "), "profiler.WithAgentlessUpload")
})
t.Run("options/BadAPIKey", func(t *testing.T) {
@@ -122,7 +145,8 @@ func TestStartStopIdempotency(t *testing.T) {
go func() {
defer wg.Done()
for j := 0; j < 1000; j++ {
- Start()
+ // startup logging makes this test very slow
+ Start(WithLogStartup(false))
}
}()
}