diff --git a/cmd/mockwallarm/main.go b/cmd/mockwallarm/main.go new file mode 100644 index 0000000..a721008 --- /dev/null +++ b/cmd/mockwallarm/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" +) + +// MockWallarm is a test server that simulates a Wallarm node. +// Usage: go run mock_wallarm.go [--mode=allow|block|slow|flaky] +// +// Modes: +// allow — always return "allow" verdict +// block — always return "block" verdict +// slow — delay 5s then allow (tests timeout) +// flaky — succeed 3 times, then fail 3 times (tests circuit breaker) + +type Verdict struct { + Verdict string `json:"verdict"` + Reason string `json:"reason,omitempty"` + Score int `json:"score,omitempty"` +} + +var ( + requestCount int + mode string +) + +func main() { + mode = "allow" + if len(os.Args) > 1 { + mode = os.Args[1] + } + // Also support --mode=block style + if len(mode) > 7 && mode[:7] == "--mode=" { + mode = mode[7:] + } + + http.HandleFunc("/inspect", handleInspect) + http.HandleFunc("/health", handleHealth) + + port := "8085" + if p := os.Getenv("PORT"); p != "" { + port = p + } + + log.Printf("Mock Wallarm node starting on :%s (mode=%s)", port, mode) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +} + +func handleInspect(w http.ResponseWriter, r *http.Request) { + requestCount++ + + switch mode { + case "block": + writeVerdict(w, "block", "SQL injection detected", 95) + + case "slow": + time.Sleep(5 * time.Second) + writeVerdict(w, "allow", "clean", 0) + + case "flaky": + if requestCount <= 3 { + writeVerdict(w, "allow", "clean", 0) + } else { + // Simulate failure — return 500 + http.Error(w, "internal error", 500) + } + if requestCount >= 6 { + requestCount = 0 // reset cycle + } + + default: // allow + writeVerdict(w, "allow", fmt.Sprintf("clean (request #%d)", requestCount), 0) + } +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func writeVerdict(w http.ResponseWriter, verdict, reason string, score int) { + writeJSON(w, http.StatusOK, Verdict{Verdict: verdict, Reason: reason, Score: score}) +} + +func writeJSON(w http.ResponseWriter, status int, v interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(v) +} diff --git a/main.go b/main.go index ef9b329..d66e1e7 100644 --- a/main.go +++ b/main.go @@ -116,20 +116,7 @@ func handleSaveConfig(w http.ResponseWriter, r *http.Request) { } func handleGetStats(w http.ResponseWriter, r *http.Request) { - state := breaker.State() - var stateStr string - switch state { - case StateClosed: - stateStr = "closed" - case StateOpen: - stateStr = "open" - case StateHalfOpen: - stateStr = "half-open" - } - writeJSON(w, http.StatusOK, map[string]interface{}{ - "circuit": stateStr, - "enabled": getConfig().Enabled, - }) + writeJSON(w, http.StatusOK, metricsSnapshot()) } func handleReturnVerdict(w http.ResponseWriter, r *http.Request) { diff --git a/middleware.go b/middleware.go index 54ffd93..38f0cc0 100644 --- a/middleware.go +++ b/middleware.go @@ -4,27 +4,60 @@ import ( "fmt" "log" "net/http" + "sync/atomic" "zoraxy-waf/zoraxy_plugin" ) var breaker = newCircuitBreaker() +// Metrics counters +var ( + requestsTotal uint64 + requestsAllowed uint64 + requestsBlocked uint64 + requestsBypassed uint64 // fail-open or disabled +) + +func metricsSnapshot() map[string]interface{} { + return map[string]interface{}{ + "total": atomic.LoadUint64(&requestsTotal), + "allowed": atomic.LoadUint64(&requestsAllowed), + "blocked": atomic.LoadUint64(&requestsBlocked), + "bypassed": atomic.LoadUint64(&requestsBypassed), + "circuit": circuitStateStr(breaker.State()), + "enabled": getConfig().Enabled, + } +} + +func circuitStateStr(s CircuitState) string { + switch s { + case StateClosed: + return "closed" + case StateOpen: + return "open" + case StateHalfOpen: + return "half-open" + } + return "unknown" +} + // handleInspect is the static capture ingress handler. // Zoraxy proxies matching requests here. The plugin decides: // - Return 280 (CAPTURED) → block the request // - Return 284 (UNHANDLED) → forward as normal func handleInspect(w http.ResponseWriter, r *http.Request) { + atomic.AddUint64(&requestsTotal, 1) cfg := getConfig() if !cfg.Enabled { - // WAF disabled — pass through. + atomic.AddUint64(&requestsBypassed, 1) w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) return } state := breaker.State() if state == StateOpen { - // Fail-open: Wallarm is down, let traffic through. + atomic.AddUint64(&requestsBypassed, 1) log.Printf("WAF: breaker OPEN — fail-open, passing %s %s", r.Method, r.URL.Path) w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) return @@ -32,9 +65,9 @@ func handleInspect(w http.ResponseWriter, r *http.Request) { resp, err := inspect(r) if err != nil { + atomic.AddUint64(&requestsBypassed, 1) log.Printf("WAF: inspection error: %v", err) breaker.RecordFailure() - // Fail-open on error. w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) return } @@ -42,14 +75,14 @@ func handleInspect(w http.ResponseWriter, r *http.Request) { breaker.RecordSuccess() if resp.Verdict == "block" { + atomic.AddUint64(&requestsBlocked, 1) log.Printf("WAF: BLOCKED %s %s — reason: %s", r.Method, r.URL.Path, resp.Reason) - // Return the Zoraxy control code for "captured" (blocked). w.Header().Set("Content-Type", "text/html") w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_CAPTURED)) fmt.Fprintf(w, `

403 Forbidden

Request blocked by WAF.

`) return } - // Allowed — let Zoraxy forward normally. + atomic.AddUint64(&requestsAllowed, 1) w.WriteHeader(int(zoraxy_plugin.ControlStatusCode_UNHANDLED)) } diff --git a/web/app.js b/web/app.js index 9c1fd10..b8868ea 100644 --- a/web/app.js +++ b/web/app.js @@ -16,6 +16,10 @@ function loadStats() { (data.circuit === 'open' ? 'warn' : data.circuit === 'closed' ? 'ok' : 'warn')); $('#waf-status').text(data.enabled ? 'Active' : 'Disabled').attr('class', 'stat-value ' + (data.enabled ? 'ok' : 'off')); + $('#stat-total').text(data.total || 0); + $('#stat-allowed').text(data.allowed || 0); + $('#stat-blocked').text(data.blocked || 0); + $('#stat-bypassed').text(data.bypassed || 0); }); } diff --git a/web/index.html b/web/index.html index 3f8b979..9ab06ff 100644 --- a/web/index.html +++ b/web/index.html @@ -54,6 +54,22 @@ WAF Status: -- +
+ Total Inspected: + 0 +
+
+ Allowed: + 0 +
+
+ Blocked: + 0 +
+
+ Bypassed (fail-open): + 0 +