servewaf: each decision heuristic is its own module (closes #195)
check / check (push) Failing after 1m19s

Each check servewaf runs on a request is a type in its own file behind
one interface, heuristic, and is registered in one of four ordered lists
in heuristics.go. Order and behaviour are unchanged.

Every refusal or ban now logs heuristic, the name of the heuristic that
made it, and heuristic_figures, the figures it acted on. Both are in
docs/request-log.md, and SPEC.md names each heuristic at its step.

Model: opus-5-5
This commit is contained in:
2026-11-06 19:17:11 +00:00
parent 21cac70b50
commit 79a2a00722
36 changed files with 1009 additions and 389 deletions
+20
View File
@@ -1,6 +1,7 @@
package servewaf_test
import (
"encoding/json"
"io"
"math"
"net/http"
@@ -272,6 +273,25 @@ func wantTimings(t *testing.T, line logLine, want ...string) {
}
}
// wantHeuristic checks that line names heuristic as the one that refused the
// request or banned its client, with figures, a JSON object. A heuristic of ""
// with figures "null" is a line that names none.
func wantHeuristic(t *testing.T, line logLine, heuristic, figures string) {
t.Helper()
var want map[string]any
err := json.Unmarshal([]byte(figures), &want)
if err != nil {
t.Fatalf("decode %s: %v", figures, err)
}
if line.Heuristic != heuristic || !reflect.DeepEqual(line.HeuristicFigures, want) {
t.Errorf("log line has heuristic %q with heuristic_figures %v, want %q with %s",
line.Heuristic, line.HeuristicFigures, heuristic, figures)
}
}
// microseconds is a timing in whole microseconds.
func microseconds(milliseconds float64) int64 {
return int64(math.Round(milliseconds * 1000))