Просмотр исходного кода

Merge branch 'master' into cscli-alerts

Thibault "bui" Koechlin 1 год назад
Родитель
Сommit
6a8bd3b32d

+ 5 - 3
pkg/acquisition/modules/appsec/appsec.go

@@ -354,15 +354,17 @@ func (w *AppsecSource) appsecHandler(rw http.ResponseWriter, r *http.Request) {
 
 	w.InChan <- parsedRequest
 
+	/*
+		response is a copy of w.AppSecRuntime.Response that is safe to use.
+		As OutOfBand might still be running, the original one can be modified
+	*/
 	response := <-parsedRequest.ResponseChannel
-	statusCode := http.StatusOK
 
 	if response.InBandInterrupt {
-		statusCode = http.StatusForbidden
 		AppsecBlockCounter.With(prometheus.Labels{"source": parsedRequest.RemoteAddrNormalized, "appsec_engine": parsedRequest.AppsecEngine}).Inc()
 	}
 
-	appsecResponse := w.AppsecRuntime.GenerateResponse(response, logger)
+	statusCode, appsecResponse := w.AppsecRuntime.GenerateResponse(response, logger)
 	logger.Debugf("Response: %+v", appsecResponse)
 
 	rw.WriteHeader(statusCode)

+ 5 - 2
pkg/acquisition/modules/appsec/appsec_runner.go

@@ -226,7 +226,8 @@ func (r *AppsecRunner) handleInBandInterrupt(request *appsec.ParsedRequest) {
 	if in := request.Tx.Interruption(); in != nil {
 		r.logger.Debugf("inband rules matched : %d", in.RuleID)
 		r.AppsecRuntime.Response.InBandInterrupt = true
-		r.AppsecRuntime.Response.HTTPResponseCode = r.AppsecRuntime.Config.BlockedHTTPCode
+		r.AppsecRuntime.Response.BouncerHTTPResponseCode = r.AppsecRuntime.Config.BouncerBlockedHTTPCode
+		r.AppsecRuntime.Response.UserHTTPResponseCode = r.AppsecRuntime.Config.UserBlockedHTTPCode
 		r.AppsecRuntime.Response.Action = r.AppsecRuntime.DefaultRemediation
 
 		if _, ok := r.AppsecRuntime.RemediationById[in.RuleID]; ok {
@@ -252,7 +253,9 @@ func (r *AppsecRunner) handleInBandInterrupt(request *appsec.ParsedRequest) {
 				r.logger.Errorf("unable to generate appsec event : %s", err)
 				return
 			}
-			r.outChan <- *appsecOvlfw
+			if appsecOvlfw != nil {
+				r.outChan <- *appsecOvlfw
+			}
 		}
 
 		// Should the in band match trigger an event ?

+ 691 - 44
pkg/acquisition/modules/appsec/appsec_test.go

@@ -1,6 +1,7 @@
 package appsecacquisition
 
 import (
+	"net/http"
 	"net/url"
 	"testing"
 	"time"
@@ -21,16 +22,21 @@ Missing tests (wip):
 */
 
 type appsecRuleTest struct {
-	name             string
-	expected_load_ok bool
-	inband_rules     []appsec_rule.CustomRule
-	outofband_rules  []appsec_rule.CustomRule
-	on_load          []appsec.Hook
-	pre_eval         []appsec.Hook
-	post_eval        []appsec.Hook
-	on_match         []appsec.Hook
-	input_request    appsec.ParsedRequest
-	output_asserts   func(events []types.Event, responses []appsec.AppsecTempResponse)
+	name                   string
+	expected_load_ok       bool
+	inband_rules           []appsec_rule.CustomRule
+	outofband_rules        []appsec_rule.CustomRule
+	on_load                []appsec.Hook
+	pre_eval               []appsec.Hook
+	post_eval              []appsec.Hook
+	on_match               []appsec.Hook
+	BouncerBlockedHTTPCode int
+	UserBlockedHTTPCode    int
+	UserPassedHTTPCode     int
+	DefaultRemediation     string
+	DefaultPassAction      string
+	input_request          appsec.ParsedRequest
+	output_asserts         func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int)
 }
 
 func TestAppsecOnMatchHooks(t *testing.T) {
@@ -53,13 +59,14 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Equal(t, types.APPSEC, events[0].Type)
 				require.Equal(t, types.LOG, events[1].Type)
 				require.Len(t, responses, 1)
-				require.Equal(t, 403, responses[0].HTTPResponseCode)
-				require.Equal(t, "ban", responses[0].Action)
+				require.Equal(t, 403, responses[0].BouncerHTTPResponseCode)
+				require.Equal(t, 403, responses[0].UserHTTPResponseCode)
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
 
 			},
 		},
@@ -84,17 +91,18 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Equal(t, types.APPSEC, events[0].Type)
 				require.Equal(t, types.LOG, events[1].Type)
 				require.Len(t, responses, 1)
-				require.Equal(t, 413, responses[0].HTTPResponseCode)
-				require.Equal(t, "ban", responses[0].Action)
+				require.Equal(t, 403, responses[0].BouncerHTTPResponseCode)
+				require.Equal(t, 413, responses[0].UserHTTPResponseCode)
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
 			},
 		},
 		{
-			name:             "on_match: change action to another standard one (log)",
+			name:             "on_match: change action to a non standard one (log)",
 			expected_load_ok: true,
 			inband_rules: []appsec_rule.CustomRule{
 				{
@@ -114,7 +122,7 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Equal(t, types.APPSEC, events[0].Type)
 				require.Equal(t, types.LOG, events[1].Type)
@@ -143,16 +151,16 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Equal(t, types.APPSEC, events[0].Type)
 				require.Equal(t, types.LOG, events[1].Type)
 				require.Len(t, responses, 1)
-				require.Equal(t, "allow", responses[0].Action)
+				require.Equal(t, appsec.AllowRemediation, responses[0].Action)
 			},
 		},
 		{
-			name:             "on_match: change action to another standard one (deny/ban/block)",
+			name:             "on_match: change action to another standard one (ban)",
 			expected_load_ok: true,
 			inband_rules: []appsec_rule.CustomRule{
 				{
@@ -164,7 +172,7 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				},
 			},
 			on_match: []appsec.Hook{
-				{Filter: "IsInBand == true", Apply: []string{"SetRemediation('deny')"}},
+				{Filter: "IsInBand == true", Apply: []string{"SetRemediation('ban')"}},
 			},
 			input_request: appsec.ParsedRequest{
 				RemoteAddr: "1.2.3.4",
@@ -172,10 +180,10 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, responses, 1)
 				//note: SetAction normalizes deny, ban and block to ban
-				require.Equal(t, "ban", responses[0].Action)
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
 			},
 		},
 		{
@@ -199,10 +207,10 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, responses, 1)
 				//note: SetAction normalizes deny, ban and block to ban
-				require.Equal(t, "captcha", responses[0].Action)
+				require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
 			},
 		},
 		{
@@ -226,7 +234,7 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Equal(t, types.APPSEC, events[0].Type)
 				require.Equal(t, types.LOG, events[1].Type)
@@ -255,11 +263,11 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 1)
 				require.Equal(t, types.LOG, events[0].Type)
 				require.Len(t, responses, 1)
-				require.Equal(t, "ban", responses[0].Action)
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
 			},
 		},
 		{
@@ -283,11 +291,11 @@ func TestAppsecOnMatchHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 1)
 				require.Equal(t, types.APPSEC, events[0].Type)
 				require.Len(t, responses, 1)
-				require.Equal(t, "ban", responses[0].Action)
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
 			},
 		},
 	}
@@ -328,7 +336,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Empty(t, events)
 				require.Len(t, responses, 1)
 				require.False(t, responses[0].InBandInterrupt)
@@ -356,7 +364,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Equal(t, types.APPSEC, events[0].Type)
 
@@ -391,7 +399,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Empty(t, events)
 				require.Len(t, responses, 1)
 				require.False(t, responses[0].InBandInterrupt)
@@ -419,7 +427,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Empty(t, events)
 				require.Len(t, responses, 1)
 				require.False(t, responses[0].InBandInterrupt)
@@ -447,7 +455,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Empty(t, events)
 				require.Len(t, responses, 1)
 				require.False(t, responses[0].InBandInterrupt)
@@ -472,7 +480,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 1)
 				require.Equal(t, types.LOG, events[0].Type)
 				require.True(t, events[0].Appsec.HasOutBandMatches)
@@ -506,7 +514,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Len(t, responses, 1)
 				require.Equal(t, "foobar", responses[0].Action)
@@ -533,7 +541,7 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Len(t, responses, 1)
 				require.Equal(t, "foobar", responses[0].Action)
@@ -560,10 +568,12 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Len(t, responses, 1)
 				require.Equal(t, "foobar", responses[0].Action)
+				require.Equal(t, "foobar", appsecResponse.Action)
+				require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
 			},
 		},
 	}
@@ -574,6 +584,473 @@ func TestAppsecPreEvalHooks(t *testing.T) {
 		})
 	}
 }
+
+func TestAppsecRemediationConfigHooks(t *testing.T) {
+
+	tests := []appsecRuleTest{
+		{
+			name:             "Basic matching rule",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "SetRemediation",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			on_match: []appsec.Hook{{Apply: []string{"SetRemediation('captcha')"}}}, //rule ID is generated at runtime. If you change rule, it will break the test (:
+
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "SetRemediation",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			on_match: []appsec.Hook{{Apply: []string{"SetReturnCode(418)"}}}, //rule ID is generated at runtime. If you change rule, it will break the test (:
+
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			loadAppSecEngine(test, t)
+		})
+	}
+}
+func TestOnMatchRemediationHooks(t *testing.T) {
+	tests := []appsecRuleTest{
+		{
+			name:             "set remediation to allow with on_match hook",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			on_match: []appsec.Hook{
+				{Filter: "IsInBand == true", Apply: []string{"SetRemediation('allow')"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "set remediation to captcha + custom user code with on_match hook",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			DefaultRemediation: appsec.AllowRemediation,
+			on_match: []appsec.Hook{
+				{Filter: "IsInBand == true", Apply: []string{"SetRemediation('captcha')", "SetReturnCode(418)"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				spew.Dump(responses)
+				spew.Dump(appsecResponse)
+
+				log.Errorf("http status : %d", statusCode)
+				require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
+				require.Equal(t, http.StatusForbidden, statusCode)
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			loadAppSecEngine(test, t)
+		})
+	}
+}
+
+func TestAppsecDefaultPassRemediation(t *testing.T) {
+
+	tests := []appsecRuleTest{
+		{
+			name:             "Basic non-matching rule",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/",
+				Args:       url.Values{"foo": []string{"tutu"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.AllowRemediation, responses[0].Action)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "DefaultPassAction: pass",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/",
+				Args:       url.Values{"foo": []string{"tutu"}},
+			},
+			DefaultPassAction: "allow",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.AllowRemediation, responses[0].Action)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "DefaultPassAction: captcha",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/",
+				Args:       url.Values{"foo": []string{"tutu"}},
+			},
+			DefaultPassAction: "captcha",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
+				require.Equal(t, http.StatusOK, statusCode) //@tko: body is captcha, but as it's 200, captcha won't be showed to user
+				require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "DefaultPassHTTPCode: 200",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/",
+				Args:       url.Values{"foo": []string{"tutu"}},
+			},
+			UserPassedHTTPCode: 200,
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.AllowRemediation, responses[0].Action)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "DefaultPassHTTPCode: 200",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/",
+				Args:       url.Values{"foo": []string{"tutu"}},
+			},
+			UserPassedHTTPCode: 418,
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.AllowRemediation, responses[0].Action)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			loadAppSecEngine(test, t)
+		})
+	}
+}
+
+func TestAppsecDefaultRemediation(t *testing.T) {
+
+	tests := []appsecRuleTest{
+		{
+			name:             "Basic matching rule",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule1",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "default remediation to ban (default)",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			DefaultRemediation: "ban",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "default remediation to allow",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			DefaultRemediation: "allow",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.AllowRemediation, responses[0].Action)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "default remediation to captcha",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			DefaultRemediation: "captcha",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "custom user HTTP code",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			UserBlockedHTTPCode: 418,
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "custom remediation + HTTP code",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			UserBlockedHTTPCode: 418,
+			DefaultRemediation:  "foobar",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, "foobar", responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, "foobar", appsecResponse.Action)
+				require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			loadAppSecEngine(test, t)
+		})
+	}
+}
+
 func TestAppsecRuleMatches(t *testing.T) {
 
 	/*
@@ -601,7 +1078,7 @@ func TestAppsecRuleMatches(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"toto"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Len(t, events, 2)
 				require.Equal(t, types.APPSEC, events[0].Type)
 
@@ -632,13 +1109,172 @@ func TestAppsecRuleMatches(t *testing.T) {
 				URI:        "/urllll",
 				Args:       url.Values{"foo": []string{"tutu"}},
 			},
-			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) {
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
 				require.Empty(t, events)
 				require.Len(t, responses, 1)
 				require.False(t, responses[0].InBandInterrupt)
 				require.False(t, responses[0].OutOfBandInterrupt)
 			},
 		},
+		{
+			name:             "default remediation to allow",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			DefaultRemediation: "allow",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.AllowRemediation, responses[0].Action)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "default remediation to captcha",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			DefaultRemediation: "captcha",
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.CaptchaRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusForbidden, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "no default remediation / custom user HTTP code",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"toto"}},
+			},
+			UserBlockedHTTPCode: 418,
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Equal(t, appsec.BanRemediation, responses[0].Action)
+				require.Equal(t, http.StatusForbidden, statusCode)
+				require.Equal(t, appsec.BanRemediation, appsecResponse.Action)
+				require.Equal(t, http.StatusTeapot, appsecResponse.HTTPStatus)
+			},
+		},
+		{
+			name:             "no match but try to set remediation to captcha with on_match hook",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			on_match: []appsec.Hook{
+				{Filter: "IsInBand == true", Apply: []string{"SetRemediation('captcha')"}},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"bla"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Empty(t, events)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+			},
+		},
+		{
+			name:             "no match but try to set user HTTP code with on_match hook",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			on_match: []appsec.Hook{
+				{Filter: "IsInBand == true", Apply: []string{"SetReturnCode(418)"}},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"bla"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Empty(t, events)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+			},
+		},
+		{
+			name:             "no match but try to set  remediation with pre_eval hook",
+			expected_load_ok: true,
+			inband_rules: []appsec_rule.CustomRule{
+				{
+					Name:      "rule42",
+					Zones:     []string{"ARGS"},
+					Variables: []string{"foo"},
+					Match:     appsec_rule.Match{Type: "regex", Value: "^toto"},
+					Transform: []string{"lowercase"},
+				},
+			},
+			pre_eval: []appsec.Hook{
+				{Filter: "IsInBand == true", Apply: []string{"SetRemediationByName('rule42', 'captcha')"}},
+			},
+			input_request: appsec.ParsedRequest{
+				RemoteAddr: "1.2.3.4",
+				Method:     "GET",
+				URI:        "/urllll",
+				Args:       url.Values{"foo": []string{"bla"}},
+			},
+			output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
+				require.Empty(t, events)
+				require.Equal(t, http.StatusOK, statusCode)
+				require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
+			},
+		},
 	}
 
 	for _, test := range tests {
@@ -678,7 +1314,16 @@ func loadAppSecEngine(test appsecRuleTest, t *testing.T) {
 		outofbandRules = append(outofbandRules, strRule)
 	}
 
-	appsecCfg := appsec.AppsecConfig{Logger: logger, OnLoad: test.on_load, PreEval: test.pre_eval, PostEval: test.post_eval, OnMatch: test.on_match}
+	appsecCfg := appsec.AppsecConfig{Logger: logger,
+		OnLoad:                 test.on_load,
+		PreEval:                test.pre_eval,
+		PostEval:               test.post_eval,
+		OnMatch:                test.on_match,
+		BouncerBlockedHTTPCode: test.BouncerBlockedHTTPCode,
+		UserBlockedHTTPCode:    test.UserBlockedHTTPCode,
+		UserPassedHTTPCode:     test.UserPassedHTTPCode,
+		DefaultRemediation:     test.DefaultRemediation,
+		DefaultPassAction:      test.DefaultPassAction}
 	AppsecRuntime, err := appsecCfg.Build()
 	if err != nil {
 		t.Fatalf("unable to build appsec runtime : %s", err)
@@ -724,8 +1369,10 @@ func loadAppSecEngine(test appsecRuleTest, t *testing.T) {
 
 	runner.handleRequest(&input)
 	time.Sleep(50 * time.Millisecond)
+
+	http_status, appsecResponse := AppsecRuntime.GenerateResponse(OutputResponses[0], logger)
 	log.Infof("events : %s", spew.Sdump(OutputEvents))
 	log.Infof("responses : %s", spew.Sdump(OutputResponses))
-	test.output_asserts(OutputEvents, OutputResponses)
+	test.output_asserts(OutputEvents, OutputResponses, appsecResponse, http_status)
 
 }

+ 26 - 15
pkg/acquisition/modules/loki/internal/lokiclient/loki_client.go

@@ -25,6 +25,7 @@ type LokiClient struct {
 	t                     *tomb.Tomb
 	fail_start            time.Time
 	currentTickerInterval time.Duration
+	requestHeaders        map[string]string
 }
 
 type Config struct {
@@ -116,7 +117,7 @@ func (lc *LokiClient) queryRange(uri string, ctx context.Context, c chan *LokiQu
 		case <-lc.t.Dying():
 			return lc.t.Err()
 		case <-ticker.C:
-			resp, err := http.Get(uri)
+			resp, err := lc.Get(uri)
 			if err != nil {
 				if ok := lc.shouldRetry(); !ok {
 					return errors.Wrapf(err, "error querying range")
@@ -127,6 +128,7 @@ func (lc *LokiClient) queryRange(uri string, ctx context.Context, c chan *LokiQu
 			}
 
 			if resp.StatusCode != http.StatusOK {
+				lc.Logger.Warnf("bad HTTP response code for query range: %d", resp.StatusCode)
 				body, _ := io.ReadAll(resp.Body)
 				resp.Body.Close()
 				if ok := lc.shouldRetry(); !ok {
@@ -215,7 +217,7 @@ func (lc *LokiClient) Ready(ctx context.Context) error {
 			return lc.t.Err()
 		case <-tick.C:
 			lc.Logger.Debug("Checking if Loki is ready")
-			resp, err := http.Get(url)
+			resp, err := lc.Get(url)
 			if err != nil {
 				lc.Logger.Warnf("Error checking if Loki is ready: %s", err)
 				continue
@@ -251,10 +253,9 @@ func (lc *LokiClient) Tail(ctx context.Context) (chan *LokiResponse, error) {
 	}
 
 	requestHeader := http.Header{}
-	for k, v := range lc.config.Headers {
+	for k, v := range lc.requestHeaders {
 		requestHeader.Add(k, v)
 	}
-	requestHeader.Set("User-Agent", "Crowdsec "+cwversion.VersionStr())
 	lc.Logger.Infof("Connecting to %s", u)
 	conn, _, err := dialer.Dial(u, requestHeader)
 
@@ -293,16 +294,6 @@ func (lc *LokiClient) QueryRange(ctx context.Context, infinite bool) chan *LokiQ
 
 	lc.Logger.Debugf("Since: %s (%s)", lc.config.Since, time.Now().Add(-lc.config.Since))
 
-	requestHeader := http.Header{}
-	for k, v := range lc.config.Headers {
-		requestHeader.Add(k, v)
-	}
-
-	if lc.config.Username != "" || lc.config.Password != "" {
-		requestHeader.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(lc.config.Username+":"+lc.config.Password)))
-	}
-
-	requestHeader.Set("User-Agent", "Crowdsec "+cwversion.VersionStr())
 	lc.Logger.Infof("Connecting to %s", url)
 	lc.t.Go(func() error {
 		return lc.queryRange(url, ctx, c, infinite)
@@ -310,6 +301,26 @@ func (lc *LokiClient) QueryRange(ctx context.Context, infinite bool) chan *LokiQ
 	return c
 }
 
+// Create a wrapper for http.Get to be able to set headers and auth
+func (lc *LokiClient) Get(url string) (*http.Response, error) {
+	request, err := http.NewRequest(http.MethodGet, url, nil)
+	if err != nil {
+		return nil, err
+	}
+	for k, v := range lc.requestHeaders {
+		request.Header.Add(k, v)
+	}
+	return http.DefaultClient.Do(request)
+}
+
 func NewLokiClient(config Config) *LokiClient {
-	return &LokiClient{Logger: log.WithField("component", "lokiclient"), config: config}
+	headers := make(map[string]string)
+	for k, v := range config.Headers {
+		headers[k] = v
+	}
+	if config.Username != "" || config.Password != "" {
+		headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(config.Username+":"+config.Password))
+	}
+	headers["User-Agent"] = "Crowdsec " + cwversion.VersionStr()
+	return &LokiClient{Logger: log.WithField("component", "lokiclient"), config: config, requestHeaders: headers}
 }

+ 22 - 11
pkg/acquisition/modules/loki/loki_test.go

@@ -276,10 +276,17 @@ func feedLoki(logger *log.Entry, n int, title string) error {
 	if err != nil {
 		return err
 	}
-	resp, err := http.Post("http://127.0.0.1:3100/loki/api/v1/push", "application/json", bytes.NewBuffer(buff))
+	req, err := http.NewRequest(http.MethodPost, "http://127.0.0.1:3100/loki/api/v1/push", bytes.NewBuffer(buff))
 	if err != nil {
 		return err
 	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("X-Scope-OrgID", "1234")
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
 	if resp.StatusCode != http.StatusNoContent {
 		b, _ := io.ReadAll(resp.Body)
 		logger.Error(string(b))
@@ -306,6 +313,8 @@ mode: cat
 source: loki
 url: http://127.0.0.1:3100
 query: '{server="demo",key="%s"}'
+headers:
+ x-scope-orgid: "1234"
 since: 1h
 `, title),
 		},
@@ -362,26 +371,26 @@ func TestStreamingAcquisition(t *testing.T) {
 	}{
 		{
 			name: "Bad port",
-			config: `
-mode: tail
+			config: `mode: tail
 source: loki
-url: http://127.0.0.1:3101
+url: "http://127.0.0.1:3101"
+headers:
+  x-scope-orgid: "1234"
 query: >
-  {server="demo"}
-`, // No Loki server here
+  {server="demo"}`, // No Loki server here
 			expectedErr:   "",
 			streamErr:     `loki is not ready: context deadline exceeded`,
 			expectedLines: 0,
 		},
 		{
 			name: "ok",
-			config: `
-mode: tail
+			config: `mode: tail
 source: loki
-url: http://127.0.0.1:3100
+url: "http://127.0.0.1:3100"
+headers:
+  x-scope-orgid: "1234"
 query: >
-  {server="demo"}
-`,
+  {server="demo"}`,
 			expectedErr:   "",
 			streamErr:     "",
 			expectedLines: 20,
@@ -456,6 +465,8 @@ func TestStopStreaming(t *testing.T) {
 mode: tail
 source: loki
 url: http://127.0.0.1:3100
+headers:
+  x-scope-orgid: "1234"
 query: >
   {server="demo"}
 `

+ 76 - 70
pkg/appsec/appsec.go

@@ -2,6 +2,7 @@ package appsec
 
 import (
 	"fmt"
+	"net/http"
 	"os"
 	"regexp"
 
@@ -30,6 +31,12 @@ const (
 	hookOnMatch
 )
 
+const (
+	BanRemediation     = "ban"
+	CaptchaRemediation = "captcha"
+	AllowRemediation   = "allow"
+)
+
 func (h *Hook) Build(hookStage int) error {
 
 	ctx := map[string]interface{}{}
@@ -62,12 +69,13 @@ func (h *Hook) Build(hookStage int) error {
 }
 
 type AppsecTempResponse struct {
-	InBandInterrupt    bool
-	OutOfBandInterrupt bool
-	Action             string //allow, deny, captcha, log
-	HTTPResponseCode   int
-	SendEvent          bool //do we send an internal event on rule match
-	SendAlert          bool //do we send an alert on rule match
+	InBandInterrupt         bool
+	OutOfBandInterrupt      bool
+	Action                  string //allow, deny, captcha, log
+	UserHTTPResponseCode    int    //The response code to send to the user
+	BouncerHTTPResponseCode int    //The response code to send to the remediation component
+	SendEvent               bool   //do we send an internal event on rule match
+	SendAlert               bool   //do we send an alert on rule match
 }
 
 type AppsecSubEngineOpts struct {
@@ -110,31 +118,33 @@ type AppsecRuntimeConfig struct {
 }
 
 type AppsecConfig struct {
-	Name               string              `yaml:"name"`
-	OutOfBandRules     []string            `yaml:"outofband_rules"`
-	InBandRules        []string            `yaml:"inband_rules"`
-	DefaultRemediation string              `yaml:"default_remediation"`
-	DefaultPassAction  string              `yaml:"default_pass_action"`
-	BlockedHTTPCode    int                 `yaml:"blocked_http_code"`
-	PassedHTTPCode     int                 `yaml:"passed_http_code"`
-	OnLoad             []Hook              `yaml:"on_load"`
-	PreEval            []Hook              `yaml:"pre_eval"`
-	PostEval           []Hook              `yaml:"post_eval"`
-	OnMatch            []Hook              `yaml:"on_match"`
-	VariablesTracking  []string            `yaml:"variables_tracking"`
-	InbandOptions      AppsecSubEngineOpts `yaml:"inband_options"`
-	OutOfBandOptions   AppsecSubEngineOpts `yaml:"outofband_options"`
+	Name                   string   `yaml:"name"`
+	OutOfBandRules         []string `yaml:"outofband_rules"`
+	InBandRules            []string `yaml:"inband_rules"`
+	DefaultRemediation     string   `yaml:"default_remediation"`
+	DefaultPassAction      string   `yaml:"default_pass_action"`
+	BouncerBlockedHTTPCode int      `yaml:"blocked_http_code"`      //returned to the bouncer
+	BouncerPassedHTTPCode  int      `yaml:"passed_http_code"`       //returned to the bouncer
+	UserBlockedHTTPCode    int      `yaml:"user_blocked_http_code"` //returned to the user
+	UserPassedHTTPCode     int      `yaml:"user_passed_http_code"`  //returned to the user
+
+	OnLoad            []Hook              `yaml:"on_load"`
+	PreEval           []Hook              `yaml:"pre_eval"`
+	PostEval          []Hook              `yaml:"post_eval"`
+	OnMatch           []Hook              `yaml:"on_match"`
+	VariablesTracking []string            `yaml:"variables_tracking"`
+	InbandOptions     AppsecSubEngineOpts `yaml:"inband_options"`
+	OutOfBandOptions  AppsecSubEngineOpts `yaml:"outofband_options"`
 
 	LogLevel *log.Level `yaml:"log_level"`
 	Logger   *log.Entry `yaml:"-"`
 }
 
 func (w *AppsecRuntimeConfig) ClearResponse() {
-	w.Logger.Debugf("#-> %p", w)
 	w.Response = AppsecTempResponse{}
-	w.Logger.Debugf("-> %p", w.Config)
 	w.Response.Action = w.Config.DefaultPassAction
-	w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
+	w.Response.BouncerHTTPResponseCode = w.Config.BouncerPassedHTTPCode
+	w.Response.UserHTTPResponseCode = w.Config.UserPassedHTTPCode
 	w.Response.SendEvent = true
 	w.Response.SendAlert = true
 }
@@ -191,24 +201,35 @@ func (wc *AppsecConfig) GetDataDir() string {
 
 func (wc *AppsecConfig) Build() (*AppsecRuntimeConfig, error) {
 	ret := &AppsecRuntimeConfig{Logger: wc.Logger.WithField("component", "appsec_runtime_config")}
-	//set the defaults
-	switch wc.DefaultRemediation {
-	case "":
-		wc.DefaultRemediation = "ban"
-	case "ban", "captcha", "log":
-		//those are the officially supported remediation(s)
-	default:
-		wc.Logger.Warningf("default '%s' remediation of %s is none of [ban,captcha,log] ensure bouncer compatbility!", wc.DefaultRemediation, wc.Name)
+
+	if wc.BouncerBlockedHTTPCode == 0 {
+		wc.BouncerBlockedHTTPCode = http.StatusForbidden
+	}
+	if wc.BouncerPassedHTTPCode == 0 {
+		wc.BouncerPassedHTTPCode = http.StatusOK
 	}
-	if wc.BlockedHTTPCode == 0 {
-		wc.BlockedHTTPCode = 403
+
+	if wc.UserBlockedHTTPCode == 0 {
+		wc.UserBlockedHTTPCode = http.StatusForbidden
 	}
-	if wc.PassedHTTPCode == 0 {
-		wc.PassedHTTPCode = 200
+	if wc.UserPassedHTTPCode == 0 {
+		wc.UserPassedHTTPCode = http.StatusOK
 	}
 	if wc.DefaultPassAction == "" {
-		wc.DefaultPassAction = "allow"
+		wc.DefaultPassAction = AllowRemediation
 	}
+	if wc.DefaultRemediation == "" {
+		wc.DefaultRemediation = BanRemediation
+	}
+
+	//set the defaults
+	switch wc.DefaultRemediation {
+	case BanRemediation, CaptchaRemediation, AllowRemediation:
+		//those are the officially supported remediation(s)
+	default:
+		wc.Logger.Warningf("default '%s' remediation of %s is none of [%s,%s,%s] ensure bouncer compatbility!", wc.DefaultRemediation, wc.Name, BanRemediation, CaptchaRemediation, AllowRemediation)
+	}
+
 	ret.Name = wc.Name
 	ret.Config = wc
 	ret.DefaultRemediation = wc.DefaultRemediation
@@ -553,27 +574,13 @@ func (w *AppsecRuntimeConfig) SetActionByName(name string, action string) error
 func (w *AppsecRuntimeConfig) SetAction(action string) error {
 	//log.Infof("setting to %s", action)
 	w.Logger.Debugf("setting action to %s", action)
-	switch action {
-	case "allow":
-		w.Response.Action = action
-		w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
-		//@tko how should we handle this ? it seems bouncer only understand bans, but it might be misleading ?
-	case "deny", "ban", "block":
-		w.Response.Action = "ban"
-	case "log":
-		w.Response.Action = action
-		w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
-	case "captcha":
-		w.Response.Action = action
-	default:
-		w.Response.Action = action
-	}
+	w.Response.Action = action
 	return nil
 }
 
 func (w *AppsecRuntimeConfig) SetHTTPCode(code int) error {
 	w.Logger.Debugf("setting http code to %d", code)
-	w.Response.HTTPResponseCode = code
+	w.Response.UserHTTPResponseCode = code
 	return nil
 }
 
@@ -582,24 +589,23 @@ type BodyResponse struct {
 	HTTPStatus int    `json:"http_status"`
 }
 
-func (w *AppsecRuntimeConfig) GenerateResponse(response AppsecTempResponse, logger *log.Entry) BodyResponse {
-	resp := BodyResponse{}
-	//if there is no interrupt, we should allow with default code
-	if !response.InBandInterrupt {
-		resp.Action = w.Config.DefaultPassAction
-		resp.HTTPStatus = w.Config.PassedHTTPCode
-		return resp
-	}
-	resp.Action = response.Action
-	if resp.Action == "" {
-		resp.Action = w.Config.DefaultRemediation
-	}
-	logger.Debugf("action is %s", resp.Action)
+func (w *AppsecRuntimeConfig) GenerateResponse(response AppsecTempResponse, logger *log.Entry) (int, BodyResponse) {
+	var bouncerStatusCode int
 
-	resp.HTTPStatus = response.HTTPResponseCode
-	if resp.HTTPStatus == 0 {
-		resp.HTTPStatus = w.Config.BlockedHTTPCode
+	resp := BodyResponse{Action: response.Action}
+	if response.Action == AllowRemediation {
+		resp.HTTPStatus = w.Config.UserPassedHTTPCode
+		bouncerStatusCode = w.Config.BouncerPassedHTTPCode
+	} else { //ban, captcha and anything else
+		resp.HTTPStatus = response.UserHTTPResponseCode
+		if resp.HTTPStatus == 0 {
+			resp.HTTPStatus = w.Config.UserBlockedHTTPCode
+		}
+		bouncerStatusCode = response.BouncerHTTPResponseCode
+		if bouncerStatusCode == 0 {
+			bouncerStatusCode = w.Config.BouncerBlockedHTTPCode
+		}
 	}
-	logger.Debugf("http status is %d", resp.HTTPStatus)
-	return resp
+
+	return bouncerStatusCode, resp
 }