Skip to content

Commit

Permalink
internal/appsec/waf: update libddwaf to 1.2.1 & attach WAF metrics to…
Browse files Browse the repository at this point in the history
… span tags (#1225)

- Update libddwaf to 1.2.1
- Add WAF rule configuration and runtime metrics retrieval
- Add WAF rule configuration and runtime metrics to spans
- Update WAF bindings and add helper functions
- Add WAF object decoder
  • Loading branch information
Hellzy committed Apr 8, 2022
1 parent 2c2051e commit 9e7309c
Show file tree
Hide file tree
Showing 15 changed files with 797 additions and 148 deletions.
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())
}
}
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())
}
}
131 changes: 131 additions & 0 deletions internal/appsec/dyngo/instrumentation/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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"
"fmt"
"sync"

"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/samplernames"
)

// 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)
}
}

// 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
//
// This is a workaround to tell the tracer that the trace was kept by AppSec.
// Passing any other value than `appsec.SamplerAppSec` has no effect.
// Customers should use `span.SetTag(ext.ManualKeep, true)` pattern
// to keep the trace, manually.
span.SetTag(ext.ManualKeep, samplernames.AppSec)
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
}
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
68 changes: 2 additions & 66 deletions internal/appsec/dyngo/instrumentation/grpcsec/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ 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"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/samplernames"
)

// SetSecurityEventTags sets the AppSec-specific span tags when a security event
Expand All @@ -26,7 +24,7 @@ func SetSecurityEventTags(span ddtrace.Span, events []json.RawMessage, addr net.
}

func setSecurityEventTags(span ddtrace.Span, events []json.RawMessage, addr net.Addr, md map[string][]string) error {
if err := setEventSpanTags(span, events); err != nil {
if err := instrumentation.SetEventSpanTags(span, events); err != nil {
return err
}
var ip string
Expand All @@ -44,65 +42,3 @@ func setSecurityEventTags(span ddtrace.Span, events []json.RawMessage, addr net.
}
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
//
// This is a workaround to tell the tracer that the trace was kept by AppSec.
// Passing any other value than `appsec.SamplerAppSec` has no effect.
// Customers should use `span.SetTag(ext.ManualKeep, true)` pattern
// to keep the trace, manually.
span.SetTag(ext.ManualKeep, samplernames.AppSec)
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
}
Loading

0 comments on commit 9e7309c

Please sign in to comment.