Skip to content

Commit f92a95e

Browse files
authored
feat(appsec): support WAF trace tagging rules (#3619)
1 parent 25642e2 commit f92a95e

File tree

9 files changed

+268
-11
lines changed

9 files changed

+268
-11
lines changed

internal/appsec/config/wafmanager.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package config
77

88
import (
9+
"bytes"
910
"encoding/json"
1011
"runtime"
1112
"sync"
@@ -146,7 +147,9 @@ func (m *WAFManager) RestoreDefaultConfig() error {
146147
return nil
147148
}
148149
var rules map[string]any
149-
if err := json.Unmarshal(m.initRules, &rules); err != nil {
150+
dec := json.NewDecoder(bytes.NewReader(m.initRules))
151+
dec.UseNumber()
152+
if err := dec.Decode(&rules); err != nil {
150153
return err
151154
}
152155
diag, err := m.AddOrUpdateConfig(defaultRulesPath, rules)

internal/appsec/emitter/waf/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func (op *ContextOperation) AbsorbDerivatives(derivatives map[string]any) {
138138
op.mu.Lock()
139139
defer op.mu.Unlock()
140140
if op.derivatives == nil {
141-
op.derivatives = make(map[string]any)
141+
op.derivatives = make(map[string]any, len(derivatives))
142142
}
143143

144144
for k, v := range derivatives {

internal/appsec/emitter/waf/run.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111
"maps"
1212

1313
"github.com/DataDog/dd-trace-go/v2/appsec/events"
14+
"github.com/DataDog/dd-trace-go/v2/ddtrace/ext"
1415
"github.com/DataDog/dd-trace-go/v2/instrumentation/appsec/dyngo"
1516
"github.com/DataDog/dd-trace-go/v2/instrumentation/appsec/emitter/waf/actions"
1617
"github.com/DataDog/dd-trace-go/v2/internal/log"
18+
"github.com/DataDog/dd-trace-go/v2/internal/samplernames"
1719
"github.com/DataDog/go-libddwaf/v4"
1820
"github.com/DataDog/go-libddwaf/v4/waferrors"
1921
)
@@ -50,6 +52,11 @@ func (op *ContextOperation) Run(eventReceiver dyngo.Operation, addrs libddwaf.Ru
5052
blocking := actions.SendActionEvents(eventReceiver, result.Actions)
5153
op.AbsorbDerivatives(result.Derivatives)
5254

55+
// Set the trace to ManualKeep if the WAF instructed us to keep it.
56+
if result.Keep {
57+
op.SetTag(ext.ManualKeep, samplernames.AppSec)
58+
}
59+
5360
if result.HasEvents() {
5461
dyngo.EmitData(op, &SecurityEvent{})
5562
}

internal/appsec/listener/grpcsec/grpc_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ func TestTags(t *testing.T) {
108108
if eventCase.events != nil {
109109
require.Subset(t, span.Tags, map[string]interface{}{
110110
"_dd.appsec.json": eventCase.expectedTag,
111-
"manual.keep": true,
112111
"appsec.event": true,
113112
"_dd.origin": "appsec",
114113
})

internal/appsec/listener/httpsec/request_test.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,29 @@
66
package httpsec
77

88
import (
9+
"context"
910
"encoding/json"
1011
"fmt"
1112
"net"
1213
"net/netip"
1314
"testing"
15+
"time"
16+
17+
_ "embed" // For go:embed
1418

1519
"github.com/stretchr/testify/require"
1620
"google.golang.org/grpc/metadata"
1721

22+
"github.com/DataDog/appsec-internal-go/apisec"
23+
"github.com/DataDog/appsec-internal-go/appsec"
1824
"github.com/DataDog/dd-trace-go/v2/ddtrace/ext"
25+
"github.com/DataDog/dd-trace-go/v2/instrumentation/appsec/dyngo"
26+
"github.com/DataDog/dd-trace-go/v2/instrumentation/appsec/emitter/httpsec"
1927
"github.com/DataDog/dd-trace-go/v2/internal"
28+
"github.com/DataDog/dd-trace-go/v2/internal/appsec/config"
2029
"github.com/DataDog/dd-trace-go/v2/internal/appsec/listener/waf"
2130
"github.com/DataDog/dd-trace-go/v2/internal/samplernames"
31+
"github.com/DataDog/go-libddwaf/v4"
2232
)
2333

2434
func TestClientIP(t *testing.T) {
@@ -227,7 +237,6 @@ func TestTags(t *testing.T) {
227237
if eventCase.events != nil {
228238
require.Subset(t, span.Tags, map[string]interface{}{
229239
"_dd.appsec.json": eventCase.expectedTag,
230-
"manual.keep": true,
231240
"appsec.event": true,
232241
"_dd.origin": "appsec",
233242
"_dd.p.ts": internal.TraceSourceTagValue{Value: internal.ASMTraceSource},
@@ -246,3 +255,90 @@ func TestTags(t *testing.T) {
246255
}
247256
}
248257
}
258+
259+
//go:embed testdata/trace_tagging_rules.json
260+
var wafRulesJSON []byte
261+
262+
func TestTraceTagging(t *testing.T) {
263+
if usable, err := libddwaf.Usable(); !usable {
264+
t.Skipf("libddwaf is not usable in this context: %v", err)
265+
}
266+
267+
wafManager, err := config.NewWAFManager(appsec.ObfuscatorConfig{}, wafRulesJSON)
268+
require.NoError(t, err)
269+
cfg := config.Config{
270+
WAFManager: wafManager,
271+
WAFTimeout: time.Hour,
272+
TraceRateLimit: 1_000,
273+
APISec: appsec.APISecConfig{Enabled: true, Sampler: apisec.NewSamplerWithInterval(0)},
274+
RC: nil,
275+
RASP: false,
276+
SupportedAddresses: config.NewAddressSet([]string{"server.request.headers.no_cookies"}),
277+
MetaStructAvailable: true,
278+
BlockingUnavailable: false,
279+
TracingAsTransport: false,
280+
}
281+
282+
rootOp := dyngo.NewRootOperation()
283+
feat, err := waf.NewWAFFeature(&cfg, rootOp)
284+
require.NoError(t, err)
285+
defer feat.Stop()
286+
287+
feat, err = NewHTTPSecFeature(&cfg, rootOp)
288+
require.NoError(t, err)
289+
defer feat.Stop()
290+
291+
type testCase struct {
292+
UserAgent string
293+
ExpectedTags map[string]any
294+
}
295+
testCases := map[string]testCase{
296+
"Attributes, No Keep, No Event": {
297+
UserAgent: "TraceTagging/v1+test",
298+
ExpectedTags: map[string]any{
299+
"_dd.appsec.trace.integer": int64(662607015),
300+
"_dd.appsec.trace.agent": "TraceTagging/v1+test",
301+
},
302+
},
303+
"Attributes, Keep, No Event": {
304+
UserAgent: "TraceTagging/v2+test",
305+
ExpectedTags: map[string]any{
306+
ext.ManualKeep: true,
307+
"_dd.appsec.trace.integer": int64(602214076),
308+
"_dd.appsec.trace.agent": "TraceTagging/v2+test",
309+
},
310+
},
311+
"Attributes, Keep, Event": {
312+
UserAgent: "TraceTagging/v3+test",
313+
ExpectedTags: map[string]any{
314+
ext.ManualKeep: true,
315+
"appsec.event": true,
316+
"_dd.appsec.trace.integer": int64(299792458),
317+
"_dd.appsec.trace.agent": "TraceTagging/v3+test",
318+
},
319+
},
320+
}
321+
for name, tc := range testCases {
322+
t.Run(name, func(t *testing.T) {
323+
ctx := context.Background()
324+
ctx = dyngo.RegisterOperation(ctx, rootOp)
325+
326+
var span MockSpan
327+
op, _, _ := httpsec.StartOperation(ctx, httpsec.HandlerOperationArgs{
328+
Framework: "test/phony",
329+
Method: "GET",
330+
RequestURI: "/fake/test/uri",
331+
RequestRoute: "/fake/:id/uri",
332+
Host: "localhost",
333+
RemoteAddr: "127.0.0.1:4242",
334+
Headers: map[string][]string{"user-agent": {tc.UserAgent}},
335+
Cookies: map[string][]string{},
336+
QueryParams: map[string][]string{},
337+
PathParams: map[string]string{"id": "test"},
338+
}, &span)
339+
op.Finish(httpsec.HandlerOperationRes{StatusCode: 200})
340+
341+
require.Subset(t, span.Tags, tc.ExpectedTags)
342+
})
343+
}
344+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
{
2+
"rules_compat": [
3+
{
4+
"id": "ttr-000-001",
5+
"name": "Trace Tagging Rule: Attributes, No Keep, No Event",
6+
"tags": {
7+
"type": "security_scanner",
8+
"category": "attack_attempt"
9+
},
10+
"conditions": [
11+
{
12+
"parameters": {
13+
"inputs": [
14+
{
15+
"address": "server.request.headers.no_cookies",
16+
"key_path": [
17+
"user-agent"
18+
]
19+
}
20+
],
21+
"regex": "^TraceTagging\\/v1"
22+
},
23+
"operator": "match_regex"
24+
}
25+
],
26+
"output": {
27+
"event": false,
28+
"keep": false,
29+
"attributes": {
30+
"_dd.appsec.trace.integer": {
31+
"value": 662607015
32+
},
33+
"_dd.appsec.trace.agent": {
34+
"address": "server.request.headers.no_cookies",
35+
"key_path": ["user-agent"]
36+
}
37+
}
38+
},
39+
"on_match": []
40+
},
41+
{
42+
"id": "ttr-000-002",
43+
"name": "Trace Tagging Rule: Attributes, Keep, No Event",
44+
"tags": {
45+
"type": "security_scanner",
46+
"category": "attack_attempt"
47+
},
48+
"conditions": [
49+
{
50+
"parameters": {
51+
"inputs": [
52+
{
53+
"address": "server.request.headers.no_cookies",
54+
"key_path": [
55+
"user-agent"
56+
]
57+
}
58+
],
59+
"regex": "^TraceTagging\\/v2"
60+
},
61+
"operator": "match_regex"
62+
}
63+
],
64+
"output": {
65+
"event": false,
66+
"keep": true,
67+
"attributes": {
68+
"_dd.appsec.trace.integer": {
69+
"value": 602214076
70+
},
71+
"_dd.appsec.trace.agent": {
72+
"address": "server.request.headers.no_cookies",
73+
"key_path": ["user-agent"]
74+
}
75+
}
76+
},
77+
"on_match": []
78+
},
79+
{
80+
"id": "ttr-000-003",
81+
"name": "Trace Tagging Rule: Attributes, Keep, Event",
82+
"tags": {
83+
"type": "security_scanner",
84+
"category": "attack_attempt"
85+
},
86+
"conditions": [
87+
{
88+
"parameters": {
89+
"inputs": [
90+
{
91+
"address": "server.request.headers.no_cookies",
92+
"key_path": [
93+
"user-agent"
94+
]
95+
}
96+
],
97+
"regex": "^TraceTagging\\/v3"
98+
},
99+
"operator": "match_regex"
100+
}
101+
],
102+
"output": {
103+
"event": true,
104+
"keep": true,
105+
"attributes": {
106+
"_dd.appsec.trace.integer": {
107+
"value": 299792458
108+
},
109+
"_dd.appsec.trace.agent": {
110+
"address": "server.request.headers.no_cookies",
111+
"key_path": ["user-agent"]
112+
}
113+
}
114+
},
115+
"on_match": []
116+
},
117+
{
118+
"id": "ttr-000-004",
119+
"name": "Trace Tagging Rule: Attributes, No Keep, Event",
120+
"tags": {
121+
"type": "security_scanner",
122+
"category": "attack_attempt"
123+
},
124+
"conditions": [
125+
{
126+
"parameters": {
127+
"inputs": [
128+
{
129+
"address": "server.request.headers.no_cookies",
130+
"key_path": [
131+
"user-agent"
132+
]
133+
}
134+
],
135+
"regex": "^TraceTagging\\/v4"
136+
},
137+
"operator": "match_regex"
138+
}
139+
],
140+
"output": {
141+
"event": true,
142+
"keep": false,
143+
"attributes": {
144+
"_dd.appsec.trace.integer": {
145+
"value": 1729
146+
},
147+
"_dd.appsec.trace.agent": {
148+
"address": "server.request.headers.no_cookies",
149+
"key_path": ["user-agent"]
150+
}
151+
}
152+
},
153+
"on_match": []
154+
}
155+
]
156+
}

internal/appsec/listener/waf/tags.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,6 @@ func AddWAFMonitoringTags(th trace.TagSetter, metrics *emitter.ContextMetrics, r
8989

9090
// SetEventSpanTags sets the security event span tags related to an appsec event
9191
func SetEventSpanTags(span trace.TagSetter) {
92-
// Keep this span due to the security event
93-
//
94-
// This is a workaround to tell the tracer that the trace was kept by AppSec.
95-
// Passing any other value than `appsec.SamplerAppSec` has no effect.
96-
// Customers should use `span.SetTag(ext.ManualKeep, true)` pattern
97-
// to keep the trace, manually.
98-
span.SetTag(ext.ManualKeep, samplernames.AppSec)
9992
span.SetTag("_dd.origin", "appsec")
10093
// Set the appsec.event tag needed by the appsec backend
10194
span.SetTag("appsec.event", true)

internal/appsec/remoteconfig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ var baseCapabilities = [...]remoteconfig.Capability{
317317
remoteconfig.ASMSessionFingerprinting,
318318
remoteconfig.ASMNetworkFingerprinting,
319319
remoteconfig.ASMHeaderFingerprinting,
320+
remoteconfig.ASMTraceTaggingRules,
320321
}
321322

322323
var blockingCapabilities = [...]remoteconfig.Capability{

internal/remoteconfig/remoteconfig.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ const (
126126
APMTracingEnableLiveDebugging
127127
// ASMDDMultiConfig represents the capability to handle multiple ASM_DD configuration objects
128128
ASMDDMultiConfig
129+
// ASMTraceTaggingRules represents the capability to honor trace tagging rules
130+
ASMTraceTaggingRules
129131
)
130132

131133
// ErrClientNotStarted is returned when the remote config client is not started.

0 commit comments

Comments
 (0)