Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 79 additions & 0 deletions pkg/acquisition/modules/appsec/appsec_hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,85 @@ func TestAppsecPreEvalHooks(t *testing.T) {
require.Equal(t, "foobar", responses[0].Action)
},
},
{
name: "pre_eval : drop request helper",
expected_load_ok: true,
pre_eval: []appsec.Hook{
{Apply: []string{"DropRequest('drop requested by config')"}},
},
input_request: appsec.ParsedRequest{
RemoteAddr: "1.2.3.4",
Method: "GET",
URI: "/urllll",
Args: url.Values{"foo": []string{"toto"}},
HTTPRequest: &http.Request{Host: "example.com"},
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, events, 3)
require.Equal(t, pipeline.APPSEC, events[0].Type)
require.Equal(t, pipeline.LOG, events[1].Type)
require.Equal(t, pipeline.LOG, events[2].Type)
require.True(t, events[1].Appsec.HasInBandMatches)
require.True(t, events[2].Appsec.HasOutBandMatches)
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
require.Equal(t, appsec.BanRemediation, responses[0].Action)
require.Equal(t, 403, responses[0].UserHTTPResponseCode)
require.Equal(t, 403, responses[0].BouncerHTTPResponseCode)
require.Len(t, events[1].Appsec.MatchedRules, 1)
require.Equal(t, "drop requested by config", events[1].Appsec.MatchedRules[0]["name"])
require.Equal(t, "drop requested by config", events[1].Appsec.MatchedRules[0]["msg"])
require.Equal(t, "drop requested by config", events[1].Parsed["appsec_drop_reason"])
},
},
{
name: "pre_eval : set remediation and return code",
expected_load_ok: true,
pre_eval: []appsec.Hook{
{Apply: []string{"SetRemediation('log')", "SetReturnCode(418)"}},
},
input_request: appsec.ParsedRequest{
RemoteAddr: "1.2.3.4",
Method: "GET",
URI: "/urllll",
Args: url.Values{"foo": []string{"toto"}},
HTTPRequest: &http.Request{Host: "example.com"},
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Empty(t, events)
require.Len(t, responses, 1)
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
require.Equal(t, 200, responses[0].UserHTTPResponseCode)
},
},
{
name: "pre_eval : drop helper with remediation override",
expected_load_ok: true,
pre_eval: []appsec.Hook{
{Apply: []string{"SetRemediation('log')", "SetReturnCode(418)", "DropRequest('drop requested by config')"}},
},
input_request: appsec.ParsedRequest{
RemoteAddr: "1.2.3.4",
Method: "GET",
URI: "/urllll",
Args: url.Values{"foo": []string{"toto"}},
HTTPRequest: &http.Request{Host: "example.com"},
},
output_asserts: func(events []pipeline.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, events, 3)
require.Equal(t, pipeline.APPSEC, events[0].Type)
require.Equal(t, pipeline.LOG, events[1].Type)
require.Equal(t, pipeline.LOG, events[2].Type)
require.True(t, events[1].Appsec.HasInBandMatches)
require.True(t, events[2].Appsec.HasOutBandMatches)
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
require.Equal(t, "log", responses[0].Action)
require.Equal(t, 418, responses[0].UserHTTPResponseCode)
require.Equal(t, "drop requested by config", events[1].Appsec.MatchedRules[0]["name"])
require.Equal(t, "drop requested by config", events[2].Appsec.MatchedRules[0]["name"])
},
},
{
name: "pre_eval : set remediation by ID",
expected_load_ok: true,
Expand Down
159 changes: 99 additions & 60 deletions pkg/acquisition/modules/appsec/appsec_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ func (r *AppsecRunner) processRequest(state *appsec.AppsecRequestState, request
//FIXME: should we abort here ?
}

if state.DropInfo(request) != nil {
r.logger.Debug("drop helper triggered during pre_eval, skipping WAF evaluation")
return nil
}

state.Tx.ProcessConnection(request.ClientIP, 0, "", 0)

for k, v := range request.Args {
Expand Down Expand Up @@ -217,7 +222,8 @@ func (r *AppsecRunner) processRequest(state *appsec.AppsecRequestState, request
func (r *AppsecRunner) ProcessInBandRules(state *appsec.AppsecRequestState, request *appsec.ParsedRequest) error {
tx := appsec.NewExtendedTransaction(r.AppsecInbandEngine, request.UUID)
state.Tx = tx
if len(r.AppsecRuntime.InBandRules) == 0 {
// Even if we have no inband rules, we might have pre-eval rules to process
if len(r.AppsecRuntime.InBandRules) == 0 && len(r.AppsecRuntime.CompiledPreEval) == 0 {
return nil
}
err := r.processRequest(state, request)
Expand All @@ -227,7 +233,7 @@ func (r *AppsecRunner) ProcessInBandRules(state *appsec.AppsecRequestState, requ
func (r *AppsecRunner) ProcessOutOfBandRules(state *appsec.AppsecRequestState, request *appsec.ParsedRequest) error {
tx := appsec.NewExtendedTransaction(r.AppsecOutbandEngine, request.UUID)
state.Tx = tx
if len(r.AppsecRuntime.OutOfBandRules) == 0 {
if len(r.AppsecRuntime.OutOfBandRules) == 0 && len(r.AppsecRuntime.CompiledPreEval) == 0 {
return nil
}
err := r.processRequest(state, request)
Expand All @@ -247,48 +253,63 @@ func (r *AppsecRunner) handleInBandInterrupt(state *appsec.AppsecRequestState, r
//let's not interrupt the pipeline for this
r.logger.Errorf("unable to create event from request : %s", err)
}

r.AccumulateTxToEvent(&evt, state, request)

if in := state.Tx.Interruption(); in != nil {
r.logger.Debugf("inband rules matched : %d", in.RuleID)
state.Response.InBandInterrupt = true
state.Response.BouncerHTTPResponseCode = r.AppsecRuntime.Config.BouncerBlockedHTTPCode
state.Response.UserHTTPResponseCode = r.AppsecRuntime.Config.UserBlockedHTTPCode
state.Response.Action = r.AppsecRuntime.DefaultRemediation
interrupt := state.Tx.Interruption()
dropInfo := state.InBandDrop
if interrupt == nil && dropInfo == nil {
return
}
if interrupt == nil && dropInfo != nil {
interrupt = dropInfo.Interruption
}

if dropInfo != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge the ifs for clarity ?

r.logger.Debugf("inband drop helper triggered: %s", dropInfo.Reason)
} else {
r.logger.Debugf("inband rules matched : %d", interrupt.RuleID)
}

if _, ok := r.AppsecRuntime.RemediationById[in.RuleID]; ok {
state.Response.Action = r.AppsecRuntime.RemediationById[in.RuleID]
}
state.Response.InBandInterrupt = true
state.Response.BouncerHTTPResponseCode = r.AppsecRuntime.Config.BouncerBlockedHTTPCode
state.Response.UserHTTPResponseCode = r.AppsecRuntime.Config.UserBlockedHTTPCode
state.Response.Action = r.AppsecRuntime.DefaultRemediation
state.ApplyPendingResponse()

for tag, remediation := range r.AppsecRuntime.RemediationByTag {
if slices.Contains(in.Tags, tag) {
state.Response.Action = remediation
}
if _, ok := r.AppsecRuntime.RemediationById[interrupt.RuleID]; ok {
state.Response.Action = r.AppsecRuntime.RemediationById[interrupt.RuleID]
}

for tag, remediation := range r.AppsecRuntime.RemediationByTag {
if slices.Contains(interrupt.Tags, tag) {
state.Response.Action = remediation
}
}

if dropInfo != nil && dropInfo.Reason != "" {
evt.Meta["appsec_drop_reason"] = dropInfo.Reason
}

err = r.AppsecRuntime.ProcessOnMatchRules(state, request, evt)
if err != nil {
r.logger.Errorf("unable to process OnMatch rules: %s", err)
return
}

err = r.AppsecRuntime.ProcessOnMatchRules(state, request, evt)
// Should the in band match trigger an overflow ?
if state.Response.SendAlert {
appsecOvlfw, err := AppsecEventGeneration(evt, request.HTTPRequest)
if err != nil {
r.logger.Errorf("unable to process OnMatch rules: %s", err)
r.logger.Errorf("unable to generate appsec event : %s", err)
return
}

// Should the in band match trigger an overflow ?
if state.Response.SendAlert {
appsecOvlfw, err := AppsecEventGeneration(evt, request.HTTPRequest)
if err != nil {
r.logger.Errorf("unable to generate appsec event : %s", err)
return
}
if appsecOvlfw != nil {
r.outChan <- *appsecOvlfw
}
}
// Should the in band match trigger an event ?
if state.Response.SendEvent {
r.outChan <- evt
if appsecOvlfw != nil {
r.outChan <- *appsecOvlfw
}

}
// Should the in band match trigger an event ?
if state.Response.SendEvent {
r.outChan <- evt
}
}

Expand All @@ -304,38 +325,56 @@ func (r *AppsecRunner) handleOutBandInterrupt(state *appsec.AppsecRequestState,
//let's not interrupt the pipeline for this
r.logger.Errorf("unable to create event from request : %s", err)
}

r.AccumulateTxToEvent(&evt, state, request)
interrupt := state.Tx.Interruption()
dropInfo := state.OutOfBandDrop
if interrupt == nil && dropInfo == nil {
return
}
if interrupt == nil && dropInfo != nil {
interrupt = dropInfo.Interruption
}

if dropInfo != nil {
r.logger.Debugf("out-of-band drop helper triggered: %s", dropInfo.Reason)
} else {
r.logger.Debugf("outband rules matched : %d", interrupt.RuleID)
}

state.Response.OutOfBandInterrupt = true
state.ApplyPendingResponse()

if in := state.Tx.Interruption(); in != nil {
r.logger.Debugf("outband rules matched : %d", in.RuleID)
state.Response.OutOfBandInterrupt = true
if dropInfo != nil && dropInfo.Reason != "" {
if evt.Meta == nil {
evt.Meta = map[string]string{}
}
evt.Meta["appsec_drop_reason"] = dropInfo.Reason
}

err = r.AppsecRuntime.ProcessOnMatchRules(state, request, evt)
if err != nil {
r.logger.Errorf("unable to process OnMatch rules: %s", err)
return
}

err = r.AppsecRuntime.ProcessOnMatchRules(state, request, evt)
// The alert needs to be sent first:
// The event and the alert share the same internal map (parsed, meta, ...)
// The event can be modified by the parsers, which might cause a concurrent map read/write
// Should the match trigger an overflow ?
if state.Response.SendAlert {
appsecOvlfw, err := AppsecEventGeneration(evt, request.HTTPRequest)
if err != nil {
r.logger.Errorf("unable to process OnMatch rules: %s", err)
r.logger.Errorf("unable to generate appsec event : %s", err)
return
}

// The alert needs to be sent first:
// The event and the alert share the same internal map (parsed, meta, ...)
// The event can be modified by the parsers, which might cause a concurrent map read/write
// Should the match trigger an overflow ?
if state.Response.SendAlert {
appsecOvlfw, err := AppsecEventGeneration(evt, request.HTTPRequest)
if err != nil {
r.logger.Errorf("unable to generate appsec event : %s", err)
return
}
if appsecOvlfw != nil {
r.outChan <- *appsecOvlfw
}
if appsecOvlfw != nil {
r.outChan <- *appsecOvlfw
}
}

// Should the match trigger an event ?
if state.Response.SendEvent {
r.outChan <- evt
}
// Should the match trigger an event ?
if state.Response.SendEvent {
r.outChan <- evt
}
}

Expand Down Expand Up @@ -371,7 +410,7 @@ func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
inBandParsingElapsed := time.Since(startInBandParsing)
metrics.AppsecInbandParsingHistogram.With(prometheus.Labels{"source": request.RemoteAddrNormalized, "appsec_engine": request.AppsecEngine}).Observe(inBandParsingElapsed.Seconds())

if state.Tx.IsInterrupted() {
if state.Tx.IsInterrupted() || state.InBandDrop != nil {
r.handleInBandInterrupt(&state, request)
}

Expand Down Expand Up @@ -410,7 +449,7 @@ func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
// time spent to process out of band rules
outOfBandParsingElapsed := time.Since(startOutOfBandParsing)
metrics.AppsecOutbandParsingHistogram.With(prometheus.Labels{"source": request.RemoteAddrNormalized, "appsec_engine": request.AppsecEngine}).Observe(outOfBandParsingElapsed.Seconds())
if state.Tx.IsInterrupted() {
if state.Tx.IsInterrupted() || state.OutOfBandDrop != nil {
r.handleOutBandInterrupt(&state, request)
}
}
Expand Down
Loading
Loading