package crowdsec import ( "bytes" "context" "fmt" "os" "strings" "github.com/jackc/pgx/v5/pgxpool" "git.netcell-it.de/projekte/edgeguard-native/internal/configgen" ) // WhitelistPath ist die aus dem Domain-Flag crowdsec_trusted gerenderte // CrowdSec-Parser-Whitelist. s02-enrich läuft vor den Scenarios, sodass // whitelisted Events gar nicht erst in http-crawl-non_statics o. Ä. zählen. const WhitelistPath = "/etc/crowdsec/parsers/s02-enrich/edgeguard-admin-hosts-whitelist.yaml" // WhitelistGenerator rendert eine host-genaue CrowdSec-Whitelist aus allen // Domains mit crowdsec_trusted=true. Vertrauenswürdige Admin-Panels (SPAs, die // pro Aktion viele /api/-Requests feuern) würden sonst das Scenario // http-crawl-non_statics auslösen und die Admin-IP bannen. No-op, wenn CrowdSec auf diesem Node nicht // installiert ist (managed-wenn-installiert). type WhitelistGenerator struct { pool *pgxpool.Pool SkipReload bool } func NewWhitelistGenerator(pool *pgxpool.Pool) *WhitelistGenerator { return &WhitelistGenerator{pool: pool} } func (g *WhitelistGenerator) Name() string { return "crowdsec-whitelist" } func (g *WhitelistGenerator) Render(ctx context.Context) error { // Managed-wenn-installiert: ohne CrowdSec kein Whitelist-File. if !IsInstalled() { return nil } hosts, err := g.trustedHosts(ctx) if err != nil { return fmt.Errorf("crowdsec-whitelist: query: %w", err) } // Direktes Schreiben (kein tmp+rename): /etc/crowdsec/parsers/... ist // root-owned, edgeguard darf nur die eine (postinst-chownte) Datei // überschreiben — analog chrony/unbound. if err := os.WriteFile(WhitelistPath, renderWhitelist(hosts), 0o644); err != nil { return fmt.Errorf("crowdsec-whitelist: write %s: %w", WhitelistPath, err) } if g.SkipReload { return nil } return configgen.ReloadService("crowdsec") } // RenderToString gibt die gerenderte Whitelist zurück (Config-Preview), ohne zu // schreiben oder zu reloaden. func (g *WhitelistGenerator) RenderToString(ctx context.Context) (string, error) { hosts, err := g.trustedHosts(ctx) if err != nil { return "", err } return string(renderWhitelist(hosts)), nil } func (g *WhitelistGenerator) trustedHosts(ctx context.Context) ([]string, error) { rows, err := g.pool.Query(ctx, `SELECT name FROM domains WHERE crowdsec_trusted = true AND active = true ORDER BY name`) if err != nil { return nil, err } defer rows.Close() var hosts []string for rows.Next() { var n string if err := rows.Scan(&n); err != nil { return nil, err } hosts = append(hosts, n) } return hosts, rows.Err() } // renderWhitelist baut die CrowdSec-Parser-Whitelist-YAML. Ohne vertrauens- // würdige Hosts bleibt die Ausdrucksliste leer → `in []` matcht nie → es wird // nichts whitelisted (Datei bleibt gültig). Pure Funktion (testbar). func renderWhitelist(hosts []string) []byte { quoted := make([]string, 0, len(hosts)) for _, h := range hosts { h = strings.TrimSpace(h) if h == "" { continue } // Einfachquote + eingebettete Quotes verdoppeln (expr-String-Literal). quoted = append(quoted, "'"+strings.ReplaceAll(h, "'", "''")+"'") } var b bytes.Buffer b.WriteString("# Generated by edgeguard-api from domains.crowdsec_trusted. DO NOT EDIT.\n") b.WriteString("name: edgeguard/admin-hosts-whitelist\n") b.WriteString("description: Trusted admin panels (SPA fires many /api/ requests) exempted from CrowdSec - not a crawl.\n") b.WriteString("whitelist:\n") b.WriteString(" reason: edgeguard trusted admin host (SPA, not crawl/probing)\n") b.WriteString(" expression:\n") fmt.Fprintf(&b, " - \"evt.Parsed.http_host in [%s]\"\n", strings.Join(quoted, ", ")) return b.Bytes() }