Skip to content

Commit

Permalink
appsec: Attacker Fingerprinting (#2899)
Browse files Browse the repository at this point in the history
Signed-off-by: Eliott Bouhana <[email protected]>
  • Loading branch information
eliottness authored Oct 1, 2024
1 parent daa6823 commit e5a17f2
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 78 deletions.
51 changes: 30 additions & 21 deletions appsec/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var appsecDisabledLog sync.Once
// Note that passing the raw bytes of the HTTP request body is not expected and would
// result in inaccurate attack detection.
// This function always returns nil when appsec is disabled.
func MonitorParsedHTTPBody(ctx context.Context, body interface{}) error {
func MonitorParsedHTTPBody(ctx context.Context, body any) error {
if !appsec.Enabled() {
appsecDisabledLog.Do(func() { log.Warn("appsec: not enabled. Body blocking checks won't be performed.") })
return nil
Expand All @@ -60,7 +60,15 @@ func SetUser(ctx context.Context, id string, opts ...tracer.UserMonitoringOption
appsecDisabledLog.Do(func() { log.Warn("appsec: not enabled. User blocking checks won't be performed.") })
return nil
}
return usersec.MonitorUser(ctx, id)

op, errPtr := usersec.StartUserLoginOperation(ctx, usersec.UserLoginOperationArgs{})
op.Finish(usersec.UserLoginOperationRes{
UserID: id,
SessionID: getSessionID(opts...),
Success: true,
})

return *errPtr
}

// TrackUserLoginSuccessEvent sets a successful user login event, with the given
Expand All @@ -76,17 +84,7 @@ func SetUser(ctx context.Context, id string, opts ...tracer.UserMonitoringOption
// Take-Over (ATO) monitoring, ultimately blocking the IP address and/or user id
// associated to them.
func TrackUserLoginSuccessEvent(ctx context.Context, uid string, md map[string]string, opts ...tracer.UserMonitoringOption) error {
span := getRootSpan(ctx)
if span == nil {
return nil
}

const tagPrefix = "appsec.events.users.login.success."
span.SetTag(tagPrefix+"track", true)
for k, v := range md {
span.SetTag(tagPrefix+k, v)
}
span.SetTag(ext.SamplingPriority, ext.PriorityUserKeep)
TrackCustomEvent(ctx, "users.login.success", md)
return SetUser(ctx, uid, opts...)
}

Expand All @@ -106,14 +104,15 @@ func TrackUserLoginFailureEvent(ctx context.Context, uid string, exists bool, md
return
}

const tagPrefix = "appsec.events.users.login.failure."
span.SetTag(tagPrefix+"track", true)
span.SetTag(tagPrefix+"usr.id", uid)
span.SetTag(tagPrefix+"usr.exists", exists)
for k, v := range md {
span.SetTag(tagPrefix+k, v)
}
span.SetTag(ext.SamplingPriority, ext.PriorityUserKeep)
// We need to do the first call to SetTag ourselves because the map taken by TrackCustomEvent is map[string]string
// and not map [string]any, so the `exists` boolean variable does not fit int
span.SetTag("appsec.events.users.login.failure.usr.exists", exists)
span.SetTag("appsec.events.users.login.failure.usr.id", uid)

TrackCustomEvent(ctx, "users.login.failure", md)

op, _ := usersec.StartUserLoginOperation(ctx, usersec.UserLoginOperationArgs{})
op.Finish(usersec.UserLoginOperationRes{UserID: uid, Success: false})
}

// TrackCustomEvent sets a custom event as service entry span tags. This span is
Expand Down Expand Up @@ -153,3 +152,13 @@ func getRootSpan(ctx context.Context) tracer.Span {
log.Error("appsec: could not access the root span")
return nil
}

func getSessionID(opts ...tracer.UserMonitoringOption) string {
cfg := &tracer.UserMonitoringConfig{
Metadata: make(map[string]string),
}
for _, opt := range opts {
opt(cfg)
}
return cfg.SessionID
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.22.0
require (
cloud.google.com/go/pubsub v1.33.0
github.com/99designs/gqlgen v0.17.36
github.com/DataDog/appsec-internal-go v1.7.0
github.com/DataDog/appsec-internal-go v1.8.0
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0
github.com/DataDog/datadog-go/v5 v5.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -625,8 +625,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9s
github.com/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/appsec-internal-go v1.7.0 h1:iKRNLih83dJeVya3IoUfK+6HLD/hQsIbyBlfvLmAeb0=
github.com/DataDog/appsec-internal-go v1.7.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
github.com/DataDog/appsec-internal-go v1.8.0 h1:1Tfn3LEogntRqZtf88twSApOCAAO3V+NILYhuQIo4J4=
github.com/DataDog/appsec-internal-go v1.8.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0 h1:LplNAmMgZvGU7kKA0+4c1xWOjz828xweW5TCi8Mw9Q0=
Expand Down
2 changes: 1 addition & 1 deletion internal/apps/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
)

require (
github.com/DataDog/appsec-internal-go v1.7.0 // indirect
github.com/DataDog/appsec-internal-go v1.8.0 // indirect
github.com/DataDog/go-libddwaf/v3 v3.4.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
Expand Down
4 changes: 2 additions & 2 deletions internal/apps/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/DataDog/appsec-internal-go v1.7.0 h1:iKRNLih83dJeVya3IoUfK+6HLD/hQsIbyBlfvLmAeb0=
github.com/DataDog/appsec-internal-go v1.7.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
github.com/DataDog/appsec-internal-go v1.8.0 h1:1Tfn3LEogntRqZtf88twSApOCAAO3V+NILYhuQIo4J4=
github.com/DataDog/appsec-internal-go v1.8.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0 h1:LplNAmMgZvGU7kKA0+4c1xWOjz828xweW5TCi8Mw9Q0=
Expand Down
49 changes: 23 additions & 26 deletions internal/appsec/emitter/usersec/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,42 @@ import (

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

const errorLog = `
appsec: user login monitoring ignored: could not find the http handler instrumentation metadata in the request context:
the request handler is not being monitored by a middleware function or the provided context is not the expected request context
`

type (
// UserIDOperation type representing a call to appsec.SetUser(). It gets both created and destroyed in a single
// UserLoginOperation type representing a call to appsec.SetUser(). It gets both created and destroyed in a single
// call to ExecuteUserIDOperation
UserIDOperation struct {
UserLoginOperation struct {
dyngo.Operation
}
// UserIDOperationArgs is the user ID operation arguments.
UserIDOperationArgs struct {
UserID string
// UserLoginOperationArgs is the user ID operation arguments.
UserLoginOperationArgs struct{}

// UserLoginOperationRes is the user ID operation results.
UserLoginOperationRes struct {
UserID string
SessionID string
Success bool
}
// UserIDOperationRes is the user ID operation results.
UserIDOperationRes struct{}
)

// ExecuteUserIDOperation starts and finishes the UserID operation by emitting a dyngo start and finish events
// An error is returned if the user associated to that operation must be blocked
func ExecuteUserIDOperation(parent dyngo.Operation, args UserIDOperationArgs) error {
func StartUserLoginOperation(ctx context.Context, args UserLoginOperationArgs) (*UserLoginOperation, *error) {
parent, _ := dyngo.FromContext(ctx)
op := &UserLoginOperation{Operation: dyngo.NewOperation(parent)}
var err error
op := &UserIDOperation{Operation: dyngo.NewOperation(parent)}
dyngo.OnData(op, func(e *events.BlockingSecurityEvent) { err = e })
dyngo.StartOperation(op, args)
dyngo.FinishOperation(op, UserIDOperationRes{})
return err
return op, &err
}

// MonitorUser starts and finishes a UserID operation.
// A call to the WAF is made to check the user ID and an error is returned if the
// user should be blocked. The return value is nil otherwise.
func MonitorUser(ctx context.Context, userID string) error {
if parent, ok := dyngo.FromContext(ctx); ok {
return ExecuteUserIDOperation(parent, UserIDOperationArgs{UserID: userID})
}
log.Error("appsec: user ID monitoring ignored: could not find the http handler instrumentation metadata in the request context: the request handler is not being monitored by a middleware function or the provided context is not the expected request context")
return nil

func (op *UserLoginOperation) Finish(args UserLoginOperationRes) {
dyngo.FinishOperation(op, args)
}

func (UserIDOperationArgs) IsArgOf(*UserIDOperation) {}
func (UserIDOperationRes) IsResultOf(*UserIDOperation) {}
func (UserLoginOperationArgs) IsArgOf(*UserLoginOperation) {}
func (UserLoginOperationRes) IsResultOf(*UserLoginOperation) {}
6 changes: 5 additions & 1 deletion internal/appsec/emitter/waf/addresses/addresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ const (
ServerResponseHeadersNoCookiesAddr = "server.response.headers.no_cookies"

ClientIPAddr = "http.client_ip"
UserIDAddr = "usr.id"

UserIDAddr = "usr.id"
UserSessionIDAddr = "usr.session_id"
UserLoginSuccessAddr = "server.business_logic.users.login.success"
UserLoginFailureAddr = "server.business_logic.users.login.failure"

ServerIoNetURLAddr = "server.io.net.url"
ServerIOFSFileAddr = "server.io.fs.file"
Expand Down
31 changes: 22 additions & 9 deletions internal/appsec/emitter/waf/addresses/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,40 +28,34 @@ func NewAddressesBuilder() *RunAddressDataBuilder {
}

func (b *RunAddressDataBuilder) WithMethod(method string) *RunAddressDataBuilder {
if method == "" {
return b
}
b.Persistent[ServerRequestMethodAddr] = method
return b
}

func (b *RunAddressDataBuilder) WithRawURI(uri string) *RunAddressDataBuilder {
if uri == "" {
return b
}
b.Persistent[ServerRequestRawURIAddr] = uri
return b
}

func (b *RunAddressDataBuilder) WithHeadersNoCookies(headers map[string][]string) *RunAddressDataBuilder {
if len(headers) == 0 {
return b
headers = nil
}
b.Persistent[ServerRequestHeadersNoCookiesAddr] = headers
return b
}

func (b *RunAddressDataBuilder) WithCookies(cookies map[string][]string) *RunAddressDataBuilder {
if len(cookies) == 0 {
return b
cookies = nil
}
b.Persistent[ServerRequestCookiesAddr] = cookies
return b
}

func (b *RunAddressDataBuilder) WithQuery(query map[string][]string) *RunAddressDataBuilder {
if len(query) == 0 {
return b
query = nil
}
b.Persistent[ServerRequestQueryAddr] = query
return b
Expand Down Expand Up @@ -115,6 +109,25 @@ func (b *RunAddressDataBuilder) WithUserID(id string) *RunAddressDataBuilder {
return b
}

func (b *RunAddressDataBuilder) WithUserSessionID(id string) *RunAddressDataBuilder {
if id == "" {
return b
}
b.Persistent[UserSessionIDAddr] = id
return b

}

func (b *RunAddressDataBuilder) WithUserLoginSuccess() *RunAddressDataBuilder {
b.Persistent[UserLoginSuccessAddr] = nil
return b
}

func (b *RunAddressDataBuilder) WithUserLoginFailure() *RunAddressDataBuilder {
b.Persistent[UserLoginFailureAddr] = nil
return b
}

func (b *RunAddressDataBuilder) WithFilePath(file string) *RunAddressDataBuilder {
if file == "" {
return b
Expand Down
26 changes: 19 additions & 7 deletions internal/appsec/listener/usersec/usec.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,32 @@ func (*Feature) String() string {
func (*Feature) Stop() {}

func NewUserSecFeature(cfg *config.Config, rootOp dyngo.Operation) (listener.Feature, error) {
if !cfg.RASP || !cfg.SupportedAddresses.AnyOf(addresses.UserIDAddr) {
if !cfg.SupportedAddresses.AnyOf(
addresses.UserIDAddr,
addresses.UserSessionIDAddr,
addresses.UserLoginSuccessAddr,
addresses.UserLoginFailureAddr) {
return nil, nil
}

feature := &Feature{}
dyngo.On(rootOp, feature.OnStart)
dyngo.OnFinish(rootOp, feature.OnFinish)
return feature, nil
}

func (*Feature) OnStart(op *usersec.UserIDOperation, args usersec.UserIDOperationArgs) {
func (*Feature) OnFinish(op *usersec.UserLoginOperation, res usersec.UserLoginOperationRes) {
builder := addresses.NewAddressesBuilder().
WithUserID(res.UserID).
WithUserSessionID(res.SessionID)

if res.Success {
builder = builder.WithUserLoginSuccess()
} else {
builder = builder.WithUserLoginFailure()
}

dyngo.EmitData(op, waf.RunEvent{
Operation: op,
RunAddressData: addresses.NewAddressesBuilder().
WithUserID(args.UserID).
Build(),
Operation: op,
RunAddressData: builder.Build(),
})
}
4 changes: 4 additions & 0 deletions internal/appsec/remoteconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ var blockingCapabilities = [...]remoteconfig.Capability{
remoteconfig.ASMCustomBlockingResponse,
remoteconfig.ASMTrustedIPs,
remoteconfig.ASMExclusionData,
remoteconfig.ASMEndpointFingerprinting,
remoteconfig.ASMSessionFingerprinting,
remoteconfig.ASMNetworkFingerprinting,
remoteconfig.ASMHeaderFingerprinting,
}

func (a *appsec) enableRCBlocking() {
Expand Down
Loading

0 comments on commit e5a17f2

Please sign in to comment.