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

chore: asm serverless upgrade #33790

Merged
merged 21 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ require (
code.cloudfoundry.org/garden v0.0.0-20210208153517-580cadd489d2
code.cloudfoundry.org/lager v2.0.0+incompatible
github.com/CycloneDX/cyclonedx-go v0.9.1
github.com/DataDog/appsec-internal-go v1.9.0
github.com/DataDog/appsec-internal-go v1.10.0
github.com/DataDog/datadog-agent/pkg/gohai v0.56.0-rc.3
github.com/DataDog/datadog-agent/pkg/obfuscate v0.63.0-devel.0.20250123185937-1feb84b482c8
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.61.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ use (
pkg/util/winutil
pkg/version
test/fakeintake
test/integration/serverless/src
genesor marked this conversation as resolved.
Show resolved Hide resolved
test/new-e2e
test/otel
tools/retry_file_dump
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/remote/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ require (
)

require (
github.com/DataDog/appsec-internal-go v1.9.0 // indirect
github.com/DataDog/appsec-internal-go v1.10.0 // indirect
github.com/DataDog/datadog-agent/comp/core/secrets v0.59.0 // indirect
github.com/DataDog/datadog-agent/pkg/api v0.61.0 // indirect
github.com/DataDog/datadog-agent/pkg/collector/check/defaults v0.59.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/remote/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 11 additions & 6 deletions pkg/serverless/appsec/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import (
"errors"
"math/rand"
"os"
"time"

appsecLog "github.com/DataDog/appsec-internal-go/log"
waf "github.com/DataDog/go-libddwaf/v3"
wafErrors "github.com/DataDog/go-libddwaf/v3/errors"
json "github.com/json-iterator/go"

"github.com/DataDog/appsec-internal-go/limiter"
Expand Down Expand Up @@ -137,7 +137,7 @@ func (a *AppSec) Close() error {

// Monitor runs the security event rules and return the events as a slice
// The monitored addresses are all persistent addresses
func (a *AppSec) Monitor(addresses map[string]any) *waf.Result {
func (a *AppSec) Monitor(addresses map[string]any) *httpsec.MonitorResult {
log.Debugf("appsec: monitoring the request context %v", addresses)
ctx, err := a.handle.NewContextWithBudget(a.cfg.WafTimeout)
if err != nil {
Expand All @@ -156,23 +156,28 @@ func (a *AppSec) Monitor(addresses map[string]any) *waf.Result {

res, err := ctx.Run(waf.RunAddressData{Persistent: addresses})
if err != nil {
if err == waf.ErrTimeout {
if err == wafErrors.ErrTimeout {
log.Debugf("appsec: waf timeout value of %s reached", a.cfg.WafTimeout)
} else {
log.Errorf("appsec: unexpected waf execution error: %v", err)
return nil
}
}

dt, _ := ctx.TotalRuntime()
stats := ctx.Stats()
if res.HasEvents() {
log.Debugf("appsec: security events found in %s: %v", time.Duration(dt), res.Events)
log.Debugf("appsec: security events found in %s: %v", stats.Timers["waf.duration_ext"], res.Events)
}
if !a.eventsRateLimiter.Allow() {
log.Debugf("appsec: security events discarded: the rate limit of %d events/s is reached", a.cfg.TraceRateLimit)
return nil
}
return &res

return &httpsec.MonitorResult{
Result: res,
Diagnostics: a.handle.Diagnostics(),
Stats: stats,
}
}

// wafHealth is a simple test helper that returns the same thing as `waf.Health`
Expand Down
6 changes: 3 additions & 3 deletions pkg/serverless/appsec/appsec_test.go
genesor marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func TestMonitor(t *testing.T) {
}
res := asm.Monitor(addresses)
require.NotNil(t, res)
require.True(t, res.HasEvents())
require.True(t, res.Result.HasEvents())
})

t.Run("api-security", func(t *testing.T) {
Expand Down Expand Up @@ -152,8 +152,8 @@ func TestMonitor(t *testing.T) {
},
})
require.NotNil(t, res)
require.True(t, res.HasDerivatives())
schema, err := json.Marshal(res.Derivatives)
require.True(t, res.Result.HasDerivatives())
schema, err := json.Marshal(res.Result.Derivatives)
require.NoError(t, err)
require.Equal(t, tc.schema, string(schema))
})
Expand Down
8 changes: 7 additions & 1 deletion pkg/serverless/appsec/httpsec/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ import (
// subprocessor monitoring the given security rules addresses and returning
// the security events that matched.
type Monitorer interface {
Monitor(addresses map[string]any) *waf.Result
Monitor(addresses map[string]any) *MonitorResult
}

type MonitorResult struct {
Result waf.Result
Diagnostics waf.Diagnostics
Stats waf.Stats
}
genesor marked this conversation as resolved.
Show resolved Hide resolved

// AppSec monitoring context including the full list of monitored HTTP values
Expand Down
22 changes: 18 additions & 4 deletions pkg/serverless/appsec/httpsec/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package httpsec

import (
"bytes"
"sync"

"github.com/DataDog/datadog-agent/pkg/aggregator"
pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/trace"
Expand Down Expand Up @@ -36,6 +37,8 @@ type ProxyLifecycleProcessor struct {
invocationEvent interface{}

demux aggregator.Demultiplexer

addRulesMonitoringTags sync.Once
genesor marked this conversation as resolved.
Show resolved Hide resolved
}

// NewProxyLifecycleProcessor returns a new httpsec proxy processor monitored with the
Expand Down Expand Up @@ -273,11 +276,22 @@ func (lp *ProxyLifecycleProcessor) spanModifier(lastReqId string, chunk *pb.Trac
log.Debug("appsec: missing span tag http.route")
}

if res := lp.appsec.Monitor(ctx.toAddresses()); res.HasEvents() {
setSecurityEventsTags(span, res.Events, reqHeaders, nil)
chunk.Priority = int32(sampler.PriorityUserKeep)
setAPISecurityTags(span, res.Derivatives)
res := lp.appsec.Monitor(ctx.toAddresses())
setWAFMonitoringTags(span, res)
if res != nil {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
// Ruleset related tags are needed only once as the ruleset data does not change.
lp.addRulesMonitoringTags.Do(func() {
setRulesMonitoringTags(span, res.Diagnostics)
chunk.Priority = int32(sampler.PriorityUserKeep)
})

if res.Result.HasEvents() {
setSecurityEventsTags(span, res.Result.Events, reqHeaders, nil)
chunk.Priority = int32(sampler.PriorityUserKeep)
setAPISecurityTags(span, res.Result.Derivatives)
genesor marked this conversation as resolved.
Show resolved Hide resolved
}
}

}

// multiOrSingle picks the first non-nil map, and returns the content formatted
Expand Down
48 changes: 41 additions & 7 deletions pkg/serverless/appsec/httpsec/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"sort"
"strings"

waf "github.com/DataDog/go-libddwaf/v3"
json "github.com/json-iterator/go"

"github.com/DataDog/appsec-internal-go/httpsec"
Expand Down Expand Up @@ -84,34 +85,35 @@ func setAppSecEnabledTags(span span) {
}

// setEventSpanTags sets the security event span tags into the service entry span.
func setEventSpanTags(span span, events []any) error {
func setEventSpanTags(span span, event []any) error {
// Set the appsec event span tag
val, err := makeEventsTagValue(events)
val, err := makeEventsTagValue(event)
genesor marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
span.SetMetaTag("_dd.appsec.json", string(val))

// Set the appsec.event tag needed by the appsec backend
span.SetMetaTag("_dd.origin", "appsec")
span.SetMetaTag("appsec.event", "true")
return nil
}

// Create the value of the security events tag.
func makeEventsTagValue(events []any) (json.RawMessage, error) {
// Create the structure to use in the `_dd.appsec.json` span tag.
v := struct {
tag, err := json.Marshal(struct {
Triggers []any `json:"triggers"`
}{Triggers: events}
tag, err := json.Marshal(v)
}{Triggers: events})
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("unexpected error while serializing the appsec event span tag: %v", err)
}
return tag, nil
}

// setSecurityEventsTags sets the AppSec-specific span tags when security events were found.
func setSecurityEventsTags(span span, events []any, headers, respHeaders map[string][]string) {
if err := setEventSpanTags(span, events); err != nil {
func setSecurityEventsTags(span span, event []any, headers, respHeaders map[string][]string) {
if err := setEventSpanTags(span, event); err != nil {
genesor marked this conversation as resolved.
Show resolved Hide resolved
log.Errorf("appsec: unexpected error while creating the appsec event tags: %v", err)
return
}
Expand Down Expand Up @@ -165,3 +167,35 @@ func setClientIPTags(span span, remoteAddr string, reqHeaders map[string][]strin
span.SetMetaTag(k, v)
}
}

// setRulesMonitoringTags adds the tags related to security rules monitoring
// It's only needed once per handle initialization as the ruleset data does not
// change over time.
func setRulesMonitoringTags(span span, wafDiags waf.Diagnostics) {
rInfo := wafDiags.Rules
if rInfo == nil {
return
}

var rulesetErrors []byte
var err error
rulesetErrors, err = json.Marshal(wafDiags.Rules.Errors)
if err != nil {
log.Error("appsec: could not marshal the waf ruleset info errors to json")
}
span.SetMetaTag("_dd.appsec.event_rules.errors", string(rulesetErrors))
span.SetMetaTag("_dd.appsec.event_rules.loaded", fmt.Sprintf("%d", len(rInfo.Loaded)))
span.SetMetaTag("_dd.appsec.event_rules.error_count", fmt.Sprintf("%d", len(rInfo.Failed)))
span.SetMetaTag("_dd.appsec.waf.version", waf.Version())
}

// setWAFMonitoringTags adds the tags related to the monitoring of the WAF performances
func setWAFMonitoringTags(span span, mRes *MonitorResult) {
// Rules version is set for every request to help the backend associate Feature duration metrics with rule version
span.SetMetaTag("_dd.appsec.event_rules.version", mRes.Diagnostics.Version)

// Report the stats sent by the Feature
for k, v := range mRes.Stats.Metrics() {
span.SetMetaTag("_dd.appsec."+k, fmt.Sprintf("%v", v))
}
}
46 changes: 41 additions & 5 deletions test/integration/serverless/log_normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,51 @@ def select__dd_appsec_json(log):

entries = []

# We want to validate that some tags are sent alongside our traces.
span_tags_to_check = [
# Found only in the first security event span (sent once per WAF handle)
"_dd.appsec.event_rules.errors",
"_dd.appsec.event_rules.loaded",
"_dd.appsec.event_rules.error_count",
"_dd.appsec.waf.version",
# Found in every span triggering a security event
"_dd.origin",
"appsec.event",
# Found in every span which ran a WAF check
"_dd.appsec.event_rules.version",
"_dd.appsec.waf.duration_ext",
]

for chunk in log["chunks"]:
for span in chunk.get("spans") or []:
meta = span.get("meta") or {}

data = meta.get("_dd.appsec.json")
if data is None:
normalized_data = {}

if data is not None:
parsed = json.loads(data, strict=False)
# The triggers may appear in any order, so we sort them by rule ID
parsed["triggers"] = sorted(parsed["triggers"], key=lambda x: x["rule"]["id"])
normalized_data["appsec.json"] = parsed

# Do not check tags if it's not an appsec span.
if "_dd.appsec.event_rules.version" in meta :
tags_checks = {}
for tag in span_tags_to_check:
if tag not in meta:
tags_checks[tag] = "NOT_FOUND"
elif not meta[tag]: # Check if the value is empty (None, "", etc.)
tags_checks[tag] = "EMPTY"
else:
tags_checks[tag] = "FOUND"
normalized_data["tags"] = tags_checks

if not normalized_data:
# Do not add entries for spans that have no appsec data
continue
parsed = json.loads(data, strict=False)
# The triggers may appear in any order, so we sort them by rule ID
parsed["triggers"] = sorted(parsed["triggers"], key=lambda x: x["rule"]["id"])
entries.append(parsed)

entries.append(normalized_data)

return entries

Expand Down Expand Up @@ -321,6 +356,7 @@ def parse_args():

print(normalize(args.logs, args.type, args.stage, args.accountid))
except Exception as e:
print(e)
err: dict[str, str | list[str]] = {
"error": "normalization raised exception",
}
Expand Down
4 changes: 4 additions & 0 deletions test/integration/serverless/snapshots/appsec-csharp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"category": "attack_attempt",
"crs_id": "942100",
"cwe": "89",
"module": "waf",
"type": "sql_injection"
}
},
Expand Down Expand Up @@ -42,6 +43,7 @@
"category": "attack_attempt",
"confidence": "1",
"cwe": "200",
"module": "waf",
"tool_name": "Arachni",
"type": "attack_tool"
}
Expand Down Expand Up @@ -79,6 +81,7 @@
"category": "attack_attempt",
"crs_id": "942100",
"cwe": "89",
"module": "waf",
"type": "sql_injection"
}
},
Expand Down Expand Up @@ -111,6 +114,7 @@
"category": "attack_attempt",
"confidence": "1",
"cwe": "200",
"module": "waf",
"tool_name": "Arachni",
"type": "attack_tool"
}
Expand Down
Loading
Loading