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 all 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())
}
}
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)
Hellzy marked this conversation as resolved.
Show resolved Hide resolved
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