Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

internal/appsec/waf: add WAF metrics #1225

Merged
merged 38 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
97f2a74
internal/appsec/waf: update libddwaf to 1.2.1 (breaking commit)
Hellzy Mar 17, 2022
9dbb709
internal/appsec/waf: add decoder first draft
Hellzy Mar 22, 2022
5fe391b
internal/appsec/waf: add simple decoder testing
Hellzy Mar 29, 2022
5f72567
internal/appsec: collect waf runtime and store metric in spans
Hellzy Mar 30, 2022
d15ab4e
internal/appsec: add rulesetInfo collection and span tag
Hellzy Mar 30, 2022
d47e1ba
internal/appsec/waf: use sync.Once
Hellzy Mar 30, 2022
94a122c
internal/appsec: unexport symbols
Hellzy Mar 30, 2022
4de55f2
internal/appsec/dyngo: rename SetMetrics to SetTags
Hellzy Mar 30, 2022
9b4a4f3
internal/appsec/dyngo: add Operation.Metrics()
Hellzy Mar 30, 2022
f640b24
contrib/labstack/echo.v4: collect appsec WAF metrics
Hellzy Mar 30, 2022
704c36a
contrib/gin-gonic: collect appsec WAF metrics
Hellzy Mar 30, 2022
aca9e63
internal/appsec: stop using time.Duration.Microseconds()
Hellzy Mar 30, 2022
8e6251f
internal/appsec/waf: add waf run w/ bindings duration metric
Hellzy Mar 30, 2022
c6e20b5
internal/appsec: improve comments, lint and formatting
Hellzy Mar 31, 2022
5026d92
internal/appsec/dyngo/insrumentation: factorize common code
Hellzy Mar 31, 2022
a8dbd52
internal/appsec/waf: retrieve and store WAF metrics for grpc
Hellzy Mar 31, 2022
58e2a7e
internal/appsec/waf: fix map key decoding erroneous check
Hellzy Apr 1, 2022
3178e5d
internal/appsec/waf: add ruleset info metrics testing
Hellzy Apr 1, 2022
1869bf4
internal/appsec/waf: add WAF run duration metric test
Hellzy Apr 1, 2022
a5e12bb
internal/appsec/waf: add error handling for nil waf objects
Hellzy Apr 1, 2022
10e2d05
internal/appsec/waf: add more decoder testing
Hellzy Apr 1, 2022
5f0f0aa
Merge branch 'v1' into francois.mazeau/libddwaf-update
Hellzy Apr 1, 2022
0ff98f4
internal/appsec: fix typos, improve comments
Hellzy Apr 1, 2022
edebe93
internal/appsec/waf: remove tags
Hellzy Apr 1, 2022
f67daba
internal/appsec: rename Metrics to Tags
Hellzy Apr 4, 2022
c308913
internal/appsec/waf: use consts for span tag keys
Hellzy Apr 4, 2022
4506170
internal/appsec/instrumentation: make TagsHolder thread safe
Hellzy Apr 4, 2022
df7d431
internal/appsec/waf: refactor tests, rename errors
Hellzy Apr 4, 2022
01ca044
internal/appsec/waf: add gostring helper and remove obsolete test
Hellzy Apr 4, 2022
5f76e74
internal/appsec/waf: simpify decoder test
Hellzy Apr 4, 2022
583dbf4
internal/appsec/waf: add nested WAF object test
Hellzy Apr 4, 2022
89f9ef8
Apply suggestions from code review
Hellzy Apr 4, 2022
69a1bbb
internal/appsec: add manual keep for ruleset tagged spans
Hellzy Apr 4, 2022
94f03f7
Merge branch 'v1' into francois.mazeau/libddwaf-update
Hellzy Apr 5, 2022
b3d51d9
internal/appsec: add helpers for operations tag addition
Hellzy Apr 6, 2022
2f5dda5
internal/appsec/waf: add WAF timeout metric
Hellzy Apr 7, 2022
f7ee85c
internal/appsec/instrumentation: share SetEventSpanTags between grpcs…
Hellzy Apr 7, 2022
06f0d21
Apply suggestions from code review
Hellzy Apr 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contrib/gin-gonic/gin/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"

"github.com/gin-gonic/gin"
Expand Down Expand Up @@ -38,5 +39,6 @@ func useAppSec(c *gin.Context, span tracer.Span) func() {
}
httpsec.SetSecurityEventTags(span, events, remoteIP, args.Headers, c.Writer.Header())
}
instrumentation.SetTags(span, op.Tags())
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
}
}
3 changes: 3 additions & 0 deletions contrib/google.golang.org/grpc/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/grpcsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"

Expand All @@ -27,6 +28,7 @@ func appsecUnaryHandlerMiddleware(span ddtrace.Span, handler grpc.UnaryHandler)
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md}, nil)
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
Expand All @@ -45,6 +47,7 @@ func appsecStreamHandlerMiddleware(span ddtrace.Span, handler grpc.StreamHandler
op := grpcsec.StartHandlerOperation(grpcsec.HandlerOperationArgs{Metadata: md}, nil)
defer func() {
events := op.Finish(grpcsec.HandlerOperationRes{})
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
Expand Down
2 changes: 2 additions & 0 deletions contrib/labstack/echo.v4/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"

"github.com/labstack/echo/v4"
Expand All @@ -33,5 +34,6 @@ func useAppSec(c echo.Context, span tracer.Span) func() {
}
httpsec.SetSecurityEventTags(span, events, remoteIP, args.Headers, c.Response().Writer.Header())
}
instrumentation.SetTags(span, op.Tags())
}
}
66 changes: 66 additions & 0 deletions internal/appsec/dyngo/instrumentation/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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 instrumentation holds code commonly used between all instrumentation declinations (currently httpsec/grpcsec).
package instrumentation

import (
"encoding/json"
"sync"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
)

// TagsHolder wraps a map holding tags. The purpose of this struct is to be used by composition in an Operation
// to allow said operation to handle tags addition/retrieval. See httpsec/http.go and grpcsec/grpc.go.
type TagsHolder struct {
tags map[string]interface{}
mu sync.Mutex
}

// NewTagsHolder returns a new instance of a TagsHolder struct.
func NewTagsHolder() TagsHolder {
return TagsHolder{tags: map[string]interface{}{}}
}

// AddTag adds the key/value pair to the tags map
func (m *TagsHolder) AddTag(k string, v interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.tags[k] = v
}

// Tags returns the tags map
func (m *TagsHolder) Tags() map[string]interface{} {
return m.tags
}

// SecurityEventsHolder is a wrapper around a thread safe security events slice. The purpose of this struct is to be
// used by composition in an Operation to allow said operation to handle security events addition/retrieval.
// See httpsec/http.go and grpcsec/grpc.go.
type SecurityEventsHolder struct {
events []json.RawMessage
mu sync.Mutex
}

// AddSecurityEvents adds the security events to the collected events list.
// Thread safe.
func (s *SecurityEventsHolder) AddSecurityEvents(events ...json.RawMessage) {
s.mu.Lock()
defer s.mu.Unlock()
s.events = append(s.events, events...)
}

// Events returns the list of stored events.
func (s *SecurityEventsHolder) Events() []json.RawMessage {
return s.events
}

// SetTags fills the span tags using the key/value pairs found in `tags`
func SetTags(span ddtrace.Span, tags map[string]interface{}) {
for k, v := range tags {
span.SetTag(k, v)
}
}
22 changes: 8 additions & 14 deletions internal/appsec/dyngo/instrumentation/grpcsec/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ package grpcsec
import (
"encoding/json"
"reflect"
"sync"

"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
)

// Abstract gRPC server handler operation definitions. It is based on two
Expand All @@ -37,9 +37,8 @@ type (
// to the operation using its AddSecurityEvent() method.
HandlerOperation struct {
dyngo.Operation

events []json.RawMessage
mu sync.Mutex
instrumentation.TagsHolder
instrumentation.SecurityEventsHolder
}
// HandlerOperationArgs is the grpc handler arguments.
HandlerOperationArgs struct {
Expand Down Expand Up @@ -74,7 +73,10 @@ type (
// 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)}
op := &HandlerOperation{
Operation: dyngo.NewOperation(parent),
TagsHolder: instrumentation.NewTagsHolder(),
}
dyngo.StartOperation(op, args)
return op
}
Expand All @@ -83,15 +85,7 @@ func StartHandlerOperation(args HandlerOperationArgs, parent dyngo.Operation) *H
// 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(events []json.RawMessage) {
op.mu.Lock()
defer op.mu.Unlock()
op.events = append(op.events, events...)
return op.Events()
}

// gRPC handler operation's start and finish event callback function types.
Expand Down
2 changes: 1 addition & 1 deletion internal/appsec/dyngo/instrumentation/grpcsec/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestUsage(t *testing.T) {
require.Equal(t, expectedMessage, res.Message)
recvFinished++

handlerOp.AddSecurityEvent([]json.RawMessage{json.RawMessage(expectedMessage)})
handlerOp.AddSecurityEvents(json.RawMessage(expectedMessage))
}))
}))

Expand Down
24 changes: 12 additions & 12 deletions internal/appsec/dyngo/instrumentation/httpsec/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
)

Expand Down Expand Up @@ -81,6 +82,7 @@ func WrapHandler(handler http.Handler, span ddtrace.Span, pathParams map[string]
status = mw.Status()
}
events := op.Finish(HandlerOperationRes{Status: status})
instrumentation.SetTags(span, op.Tags())
if len(events) == 0 {
return
}
Expand Down Expand Up @@ -129,7 +131,8 @@ func MakeHandlerOperationArgs(r *http.Request, pathParams map[string]string) Han
type (
Operation struct {
dyngo.Operation
events json.RawMessage
instrumentation.TagsHolder
instrumentation.SecurityEventsHolder
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
}

// SDKBodyOperation type representing an SDK body. It must be created with
Expand All @@ -146,7 +149,10 @@ type (
// The operation is linked to the global root operation since an HTTP operation
// is always expected to be first in the operation stack.
func StartOperation(ctx context.Context, args HandlerOperationArgs) (context.Context, *Operation) {
op := &Operation{Operation: dyngo.NewOperation(nil)}
op := &Operation{
Operation: dyngo.NewOperation(nil),
TagsHolder: instrumentation.NewTagsHolder(),
}
newCtx := context.WithValue(ctx, contextKey{}, op)
dyngo.StartOperation(op, args)
return newCtx, op
Expand All @@ -162,7 +168,10 @@ func fromContext(ctx context.Context) *Operation {
// finish event up in the operation stack.
func (op *Operation) Finish(res HandlerOperationRes) json.RawMessage {
dyngo.FinishOperation(op, res)
return op.events
if events := op.Events(); len(events) > 0 {
return events[0]
}
return json.RawMessage{}
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
}

// StartSDKBodyOperation starts the SDKBody operation and emits a start event
Expand All @@ -177,15 +186,6 @@ func (op *SDKBodyOperation) Finish() {
dyngo.FinishOperation(op, SDKBodyOperationRes{})
}

// AddSecurityEvent adds the security event to the list of events observed
// during the operation lifetime.
func (op *Operation) AddSecurityEvent(event json.RawMessage) {
// TODO(Julio-Guerra): the current situation involves only one event per
// operation. In the future, multiple events per operation will become
// possible and the append operation should be made thread-safe.
op.events = event
}

// HTTP handler operation's start and finish event callback function types.
type (
// OnHandlerOperationStart function type, called when an HTTP handler
Expand Down
54 changes: 50 additions & 4 deletions internal/appsec/waf.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,23 @@ import (
"sync/atomic"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"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"
)

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"
wafDurationTag = "_dd.appsec.waf.duration"
wafDurationExtTag = "_dd.appsec.waf.duration_ext"
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
)

// Register the WAF event listener.
func registerWAF(rules []byte, timeout time.Duration, limiter Limiter) (unreg dyngo.UnregisterFunc, err error) {
// Check the WAF is healthy
Expand Down Expand Up @@ -81,6 +91,7 @@ func registerWAF(rules []byte, timeout time.Duration, limiter Limiter) (unreg dy

// newWAFEventListener returns the WAF event listener to register in order to enable it.
func newHTTPWAFEventListener(handle *waf.Handle, addresses []string, timeout time.Duration, limiter Limiter) dyngo.EventListener {
var once sync.Once
return httpsec.OnHandlerOperationStart(func(op *httpsec.Operation, args httpsec.HandlerOperationArgs) {
var body interface{}
op.On(httpsec.OnSDKBodyOperationStart(func(op *httpsec.SDKBodyOperation, args httpsec.SDKBodyOperationArgs) {
Expand Down Expand Up @@ -126,15 +137,30 @@ func newHTTPWAFEventListener(handle *waf.Handle, addresses []string, timeout tim
values[serverResponseStatusAddr] = res.Status
}
}
wafRunStartTime := time.Now()
matches := runWAF(wafCtx, values, timeout)
overallWAFRunDuration := time.Since(wafRunStartTime)

// Log WAF metrics.
// time.Duration.Microseconds() is only as of go1.13, so we do it manually here
op.AddTag(wafDurationTag, float64(wafCtx.TotalRuntime()/1e3))
op.AddTag(wafDurationExtTag, float64(overallWAFRunDuration.Nanoseconds()/1e3))
once.Do(func() {
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
rInfo := handle.RulesetInfo()
op.AddTag(eventRulesVersionTag, rInfo.Version)
op.AddTag(eventRulesErrorsTag, rInfo.Errors)
op.AddTag(eventRulesLoadedTag, float64(rInfo.Loaded))
op.AddTag(eventRulesFailedTag, float64(rInfo.Failed))
op.AddTag(ext.ManualKeep, true)
})

// Log the attacks if any
if len(matches) == 0 {
return
}
log.Debug("appsec: attack detected by the waf")
if limiter.Allow() {
op.AddSecurityEvent(matches)
op.AddSecurityEvents(matches)
}
}))

Expand All @@ -149,8 +175,11 @@ func newGRPCWAFEventListener(handle *waf.Handle, _ []string, timeout time.Durati
// receive unlimited number of messages where we could find security events
const maxWAFEventsPerRequest = 10
var (
nbEvents uint32
logOnce sync.Once
nbEvents uint32
logOnce sync.Once
metricsOnce sync.Once
wafRunDuration waf.AtomicDuration
wafBindingsRunDuration waf.AtomicDuration

events []json.RawMessage
mu sync.Mutex
Expand Down Expand Up @@ -184,7 +213,13 @@ func newGRPCWAFEventListener(handle *waf.Handle, _ []string, timeout time.Durati
if md := handlerArgs.Metadata; len(md) > 0 {
values[grpcServerRequestMetadata] = md
}
now := time.Now()
event := runWAF(wafCtx, values, timeout)
// WAF run durations are WAF context bound. As of now we need to keep track of those externally since
// we use a new WAF context for each callback. When we are able to re-use the same WAF context across
// callbacks, we can get rid of these variables and simply use the WAF bindings in OnHandlerOperationFinish.
wafBindingsRunDuration.Add(uint64(time.Since(now).Nanoseconds()))
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
wafRunDuration.Add(wafCtx.TotalRuntime())
if len(event) == 0 {
return
}
Expand All @@ -195,8 +230,19 @@ func newGRPCWAFEventListener(handle *waf.Handle, _ []string, timeout time.Durati
mu.Unlock()
}))
op.On(grpcsec.OnHandlerOperationFinish(func(op *grpcsec.HandlerOperation, _ grpcsec.HandlerOperationRes) {
op.AddTag(wafDurationTag, float64(wafRunDuration/1e3))
op.AddTag(wafDurationExtTag, float64(wafBindingsRunDuration/1e3))
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
Hellzy marked this conversation as resolved.
Show resolved Hide resolved

metricsOnce.Do(func() {
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
rInfo := handle.RulesetInfo()
op.AddTag(eventRulesVersionTag, rInfo.Version)
op.AddTag(eventRulesErrorsTag, rInfo.Errors)
op.AddTag(eventRulesLoadedTag, float64(rInfo.Loaded))
op.AddTag(eventRulesFailedTag, float64(rInfo.Failed))
op.AddTag(ext.ManualKeep, true)
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
})
if len(events) > 0 && limiter.Allow() {
op.AddSecurityEvent(events)
op.AddSecurityEvents(events...)
}
}))
})
Expand Down
Loading