- cmd/edgeguard-waf/: neues Binary — lädt WAF-Configs aus DB, startet SPOE-Agent auf 127.0.0.1:9000, refreshed Configs alle 30s - internal/waf/engine.go: BuildEngine() — Coraza WAF aus WafConfig bauen (SecLang-Direktiven: RuleEngine, PL, CRS-Include, Exclusions, Custom) - internal/waf/manager.go: Manager — per-Hostname Coraza-Engine-Cache (thread-safe, Lazy-Init via Reload(), Port-Strip, IPv6-Brackets) - internal/waf/spoe.go: SPOEAgent — haproxy-go SPOE-Handler (src/method/path/query/ver/host/headers aus HAProxy-Vars, Coraza-Transaction, Blocking: txn.waf.status=403 setzen) - services/waf/waf.go: ListAllWithDomain() — JOIN domains+waf_configs - go.mod: coraza/v3 v3.7.0 + dropmorepackets/haproxy-go v0.0.8 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
154 lines
4.2 KiB
Go
154 lines
4.2 KiB
Go
// Package waf implements CRUD for per-domain WAF policies (waf_configs).
|
|
package waf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
|
)
|
|
|
|
var ErrNotFound = errors.New("waf config not found")
|
|
|
|
type Repo struct {
|
|
Pool *pgxpool.Pool
|
|
}
|
|
|
|
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
|
|
|
|
const baseSelect = `
|
|
SELECT id, domain_id, enabled, mode, paranoia_level,
|
|
rule_exclusions, trusted_proxies, custom_rules, updated_at
|
|
FROM waf_configs
|
|
`
|
|
|
|
func scan(row pgx.Row) (*models.WafConfig, error) {
|
|
var c models.WafConfig
|
|
err := row.Scan(
|
|
&c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel,
|
|
&c.RuleExclusions, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// List returns all WAF configs ordered by domain_id.
|
|
func (r *Repo) List(ctx context.Context) ([]models.WafConfig, error) {
|
|
rows, err := r.Pool.Query(ctx, baseSelect+" ORDER BY domain_id ASC")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]models.WafConfig, 0, 16)
|
|
for rows.Next() {
|
|
c, err := scan(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *c)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// GetByDomain returns the WAF config for a domain, or ErrNotFound.
|
|
func (r *Repo) GetByDomain(ctx context.Context, domainID int64) (*models.WafConfig, error) {
|
|
row := r.Pool.QueryRow(ctx, baseSelect+" WHERE domain_id = $1", domainID)
|
|
c, err := scan(row)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Upsert inserts or updates the WAF config for a domain.
|
|
// Returns the resulting row.
|
|
func (r *Repo) Upsert(ctx context.Context, c models.WafConfig) (*models.WafConfig, error) {
|
|
c.UpdatedAt = time.Now()
|
|
row := r.Pool.QueryRow(ctx, `
|
|
INSERT INTO waf_configs
|
|
(domain_id, enabled, mode, paranoia_level,
|
|
rule_exclusions, trusted_proxies, custom_rules, updated_at)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
|
ON CONFLICT (domain_id) DO UPDATE SET
|
|
enabled = EXCLUDED.enabled,
|
|
mode = EXCLUDED.mode,
|
|
paranoia_level = EXCLUDED.paranoia_level,
|
|
rule_exclusions = EXCLUDED.rule_exclusions,
|
|
trusted_proxies = EXCLUDED.trusted_proxies,
|
|
custom_rules = EXCLUDED.custom_rules,
|
|
updated_at = EXCLUDED.updated_at
|
|
RETURNING id, domain_id, enabled, mode, paranoia_level,
|
|
rule_exclusions, trusted_proxies, custom_rules, updated_at
|
|
`,
|
|
c.DomainID, c.Enabled, c.Mode, c.ParanoiaLevel,
|
|
c.RuleExclusions, c.TrustedProxies, c.CustomRules, c.UpdatedAt,
|
|
)
|
|
return scan(row)
|
|
}
|
|
|
|
// ListEnabled returns only configs with enabled=true (used by the WAF agent).
|
|
func (r *Repo) ListEnabled(ctx context.Context) ([]models.WafConfig, error) {
|
|
rows, err := r.Pool.Query(ctx, baseSelect+" WHERE enabled = true ORDER BY domain_id ASC")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]models.WafConfig, 0, 8)
|
|
for rows.Next() {
|
|
c, err := scan(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *c)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// DomainConfigPair combines a domain hostname with its WAF config.
|
|
type DomainConfigPair struct {
|
|
Hostname string
|
|
Config models.WafConfig
|
|
}
|
|
|
|
// ListAllWithDomain returns all WAF configs joined with their domain name.
|
|
// Used by the WAF agent to build the hostname→engine mapping.
|
|
func (r *Repo) ListAllWithDomain(ctx context.Context) ([]DomainConfigPair, error) {
|
|
rows, err := r.Pool.Query(ctx, `
|
|
SELECT d.name,
|
|
w.id, w.domain_id, w.enabled, w.mode, w.paranoia_level,
|
|
w.rule_exclusions, w.trusted_proxies, w.custom_rules, w.updated_at
|
|
FROM waf_configs w
|
|
JOIN domains d ON d.id = w.domain_id
|
|
WHERE d.active = true
|
|
ORDER BY d.name ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]DomainConfigPair, 0, 16)
|
|
for rows.Next() {
|
|
var p DomainConfigPair
|
|
var c models.WafConfig
|
|
if err := rows.Scan(
|
|
&p.Hostname,
|
|
&c.ID, &c.DomainID, &c.Enabled, &c.Mode, &c.ParanoiaLevel,
|
|
&c.RuleExclusions, &c.TrustedProxies, &c.CustomRules, &c.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
p.Config = c
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|