CrowdSec decision list fetched, kept, and its clients banned until the decision ends (closes #106)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
SWWAF_CROWDSEC_LAPI_URL and SWWAF_CROWDSEC_LAPI_KEY name an engine whose decision list, <url>/v1/decisions, is fetched every minute with the key in X-Api-Key, following no redirect, and kept as a blocklist is: used while a fetch fails, and across restarts through reputation.json. Ban decisions on an Ip or a Range end at the fetch time plus their duration. A listed client's request is refused and bans its netblock with the cause crowdsec until the decision ends; bans.json, ban notes and metrics take the cause. Judgement call: fetched every minute, not a setting. Judgement call: a crowdsec ban never lengthens a limit ban. Judgement call: a lifted crowdsec ban is remade while its decision lasts. Model: opus-5-5
This commit was merged in pull request #117.
This commit is contained in:
@@ -716,6 +716,80 @@ func TestBlocklistTriesAndCopiesKeptInReputationJSONAcrossRestarts(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrowdSecDecisionListKeptInReputationJSONAcrossARestart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const bouncerKey = "crowdsec-key-0123456789abcdef"
|
||||
|
||||
// A stand-in for the engine's local API, which bans 203.0.113.0/24 for
|
||||
// four hours, and answers only a request with its key, while it is up.
|
||||
down := new(atomic.Bool)
|
||||
engine := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path != "/v1/decisions" || r.Header.Get("X-Api-Key") != bouncerKey:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case down.Load():
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
default:
|
||||
_, _ = io.WriteString(w, `[{"duration": "4h0m0s", "origin": "crowdsec", `+
|
||||
`"scenario": "crowdsecurity/http-probing", "scope": "Range", `+
|
||||
`"type": "ban", "value": "203.0.113.0/24"}]`)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
dir := t.TempDir()
|
||||
env := map[string]string{
|
||||
listenAddr: localhost + ":0",
|
||||
upstreamURL: startApp(t),
|
||||
stateDir: dir,
|
||||
rulesDir: t.TempDir(),
|
||||
trustedProxies: localhost + "/32",
|
||||
"SWWAF_CROWDSEC_LAPI_URL": engine.URL,
|
||||
"SWWAF_CROWDSEC_LAPI_KEY": bouncerKey,
|
||||
}
|
||||
|
||||
// Once the list is fetched, the client's request bans it.
|
||||
first := runUntilStopped(t, env, func(url string) {
|
||||
for statusFrom(t, url, placed) != http.StatusForbidden {
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
})
|
||||
|
||||
ban := onlyBan(t, dir)
|
||||
if ban["netblock"] != placed+"/32" || ban["cause"] != "crowdsec" ||
|
||||
ban["reason"] != "CrowdSec's decision for crowdsecurity/http-probing" {
|
||||
t.Errorf("bans.json holds %v, want the ban for crowdsec on %s", ban, placed)
|
||||
}
|
||||
|
||||
// Restarted with the engine down, the copy kept in reputation.json bans
|
||||
// another client in the netblock from its first request.
|
||||
down.Store(true)
|
||||
|
||||
second := runUntilStopped(t, env, func(url string) {
|
||||
wantStatus(t, url, "203.0.113.10", http.StatusForbidden)
|
||||
})
|
||||
|
||||
// The key is in neither run's output, nor in a state file.
|
||||
files := []string{"bans.json", "reputation.json", "clients.json"}
|
||||
shown := make([]string, 0, len(files)+2)
|
||||
shown = append(shown, first.text(), second.text())
|
||||
|
||||
for _, name := range files {
|
||||
data, err := os.ReadFile(filepath.Join(dir, name)) //nolint:gosec // the test's
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", name, err)
|
||||
}
|
||||
|
||||
shown = append(shown, string(data))
|
||||
}
|
||||
|
||||
if all := strings.Join(shown, "\n"); strings.Contains(all, bouncerKey) {
|
||||
t.Errorf("the key is shown in the output or the state files:\n%s", all)
|
||||
}
|
||||
}
|
||||
|
||||
// wantDeniedByList checks that the request log line is of a request the
|
||||
// blocklist at listURL refused.
|
||||
func wantDeniedByList(t *testing.T, line map[string]any, listURL string) {
|
||||
|
||||
Reference in New Issue
Block a user