Files
edgeguard-native/cmd/edgeguard-waf/main.go
Debian 220d9d7050 feat(waf): Alerts — Regelübereinstimmungen in DB + UI — v1.2.77
- Migration 0038: waf_alerts-Tabelle
- AlertWriter (Buffered-Channel → async DB-Write)
- SPOE: MatchedRules → sendAlert() nach ProcessRequestHeaders()
- API: GET /waf/alerts + DELETE /waf/alerts
- WAF-Page: Tabs Domains | Alarme; Alarme-Tabelle mit Rule-ID,
  Severity, Aktion (Detected/Blocked), URI, Client-IP + Purge-Button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 10:40:43 +02:00

106 lines
2.6 KiB
Go

// Command edgeguard-waf is the per-domain WAF SPOE agent for EdgeGuard.
// HAProxy connects to it via the SPOE protocol (127.0.0.1:9000).
// It loads per-domain WAF configs from PostgreSQL and uses Coraza v3
// with the OWASP Core Rule Set to inspect HTTP requests.
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/waf"
intwaf "git.netcell-it.de/projekte/edgeguard-native/internal/waf"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
dsn := database.ConnStringFromEnv()
pool, err := database.Open(ctx, dsn)
if err != nil {
slog.Error("waf: db connect", "error", err)
os.Exit(1)
}
defer pool.Close()
if err := database.Migrate(ctx, ""); err != nil {
slog.Error("waf: migrate", "error", err)
os.Exit(1)
}
repo := waf.New(pool)
crsDir := os.Getenv("EDGEGUARD_WAF_CRS_DIR")
if crsDir == "" {
crsDir = intwaf.DefaultCRSDir
}
spoeAddr := os.Getenv("EDGEGUARD_WAF_ADDR")
if spoeAddr == "" {
spoeAddr = intwaf.DefaultSPOEAddr
}
mgr := intwaf.NewManager(crsDir)
// Initial load.
if err := reload(ctx, repo, mgr); err != nil {
slog.Error("waf: initial load", "error", err)
os.Exit(1)
}
// Periodic config refresh every 30 seconds.
go func() {
t := time.NewTicker(30 * time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
if err := reload(ctx, repo, mgr); err != nil {
slog.Warn("waf: reload", "error", err)
}
}
}
}()
alertWriter := intwaf.NewAlertWriter(pool, 512)
agent := intwaf.SPOEAgent{
Manager: mgr,
AlertWriter: alertWriter,
Addr: spoeAddr,
}
slog.Info("waf: SPOE agent starting", "addr", spoeAddr, "crs", crsDir)
if err := agent.ListenAndServe(ctx); err != nil && ctx.Err() == nil {
slog.Error("waf: SPOE agent stopped", "error", err)
os.Exit(1)
}
}
// reload fetches all domain+waf_config pairs from DB and rebuilds engines.
func reload(ctx context.Context, repo *waf.Repo, mgr *intwaf.Manager) error {
configs, err := repo.ListAllWithDomain(ctx)
if err != nil {
return err
}
domains := make([]intwaf.DomainConfig, 0, len(configs))
for _, c := range configs {
domains = append(domains, intwaf.DomainConfig{
Hostname: c.Hostname,
Config: c.Config,
})
}
return mgr.Reload(domains)
}
// Ensure models package is used (imported transitively via services/waf).
var _ = models.WafConfig{}