Alerts to a JSON webhook, with a cooldown and an hourly summary (closes #26)
check / check (push) Waiting to run
check / check (push) Waiting to run
SWWAF_ALERT_WEBHOOK_URL gets one JSON POST per alert, in SPEC.md's schema, with SWWAF_ALERT_WEBHOOK_HEADERS: ban and permanent_ban, with the ban's notes, in observe mode too, marked mode observe and worked out only when the alert would be sent; source_failure for GeoJS; file_error for a rule or state file with an error. SWWAF_ALERT_EVENTS chooses; SWWAF_ALERT_COOLDOWN holds back repeats by netblock, file or source; past SWWAF_ALERT_MAX_PER_HOUR the hour ends in one summary. A bounded queue, retried with backoff, holds up no request; a 4xx other than 408 and 429 gives the alert up. alerts.json keeps the queue, the cooldowns and the hour. Nothing shows the URL's path or query. Judgement call: the summary's event is summary, which SPEC.md omits. Judgement call: an admin's ban raises no alert. Model: opus-5-5
This commit was merged in pull request #93.
This commit is contained in:
+28
-14
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
|
||||
"sneak.berlin/go/smallwebwaf/internal/alerts"
|
||||
"sneak.berlin/go/smallwebwaf/internal/config"
|
||||
)
|
||||
|
||||
@@ -100,6 +101,8 @@ type Params struct {
|
||||
// ProcessLog receives how many rules were read, and the error in a
|
||||
// rule file edited while smallwebwaf runs.
|
||||
ProcessLog *slog.Logger
|
||||
// Alerts receive a file_error alert for that error.
|
||||
Alerts *alerts.Queue
|
||||
}
|
||||
|
||||
// Files are the rule files of a running smallwebwaf, and the rules read
|
||||
@@ -126,7 +129,7 @@ func Load(params Params) (*Files, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
rules, err := read(params.Dir)
|
||||
rules, _, err := read(params.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -223,13 +226,21 @@ func (f *Files) readAfterChanges(
|
||||
}
|
||||
|
||||
// readAgain reads the rule files again, in place of the rules loaded, or
|
||||
// logs the error that keeps the rules as they were.
|
||||
// logs the error that keeps the rules as they were, and raises a
|
||||
// file_error alert for it, for the file it is in.
|
||||
func (f *Files) readAgain() {
|
||||
rules, err := read(f.params.Dir)
|
||||
rules, path, err := read(f.params.Dir)
|
||||
if err != nil {
|
||||
f.params.ProcessLog.Error(
|
||||
"a rule file has an error, and the rules stay as they were",
|
||||
"error", err.Error())
|
||||
const kept = "a rule file has an error, and the rules stay as they were"
|
||||
|
||||
// Raised before it is logged, so that the alert is there once the
|
||||
// log line is.
|
||||
f.params.Alerts.Raise(alerts.Alert{
|
||||
Event: alerts.EventFileError,
|
||||
Reason: kept,
|
||||
Detail: map[string]any{"file": path, "error": err.Error()},
|
||||
})
|
||||
f.params.ProcessLog.Error(kept, "error", err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
@@ -246,13 +257,14 @@ func (f *Files) logRead(count int) {
|
||||
}
|
||||
|
||||
// read returns the rules of every rule file in dir, in the order of the
|
||||
// files' names, and then of their lines. A file whose name starts with a
|
||||
// dot, such as an editor's lock file .#50-app.rules, is not a rule file,
|
||||
// as a shell's *.rules would not match it.
|
||||
func read(dir string) ([]Rule, error) {
|
||||
// files' names, and then of their lines, or an error, with the path of the
|
||||
// rule file it is in, or dir. A file whose name starts with a dot, such as
|
||||
// an editor's lock file .#50-app.rules, is not a rule file, as a shell's
|
||||
// *.rules would not match it.
|
||||
func read(dir string) ([]Rule, string, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SWWAF_RULES_DIR cannot be read: %w", err)
|
||||
return nil, dir, fmt.Errorf("SWWAF_RULES_DIR cannot be read: %w", err)
|
||||
}
|
||||
|
||||
var rules []Rule
|
||||
@@ -266,13 +278,15 @@ func read(dir string) ([]Rule, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
rules, err = readFile(filepath.Join(dir, name), rules, places)
|
||||
path := filepath.Join(dir, name)
|
||||
|
||||
rules, err = readFile(path, rules, places)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, path, err
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
return rules, "", nil
|
||||
}
|
||||
|
||||
// readFile appends the rules of the rule file at path to rules. places
|
||||
|
||||
Reference in New Issue
Block a user