Neu: eigene WAF-App-Profile (benannte, wiederverwendbare Rule-ID-Ausnahme- Bündel) — zentrale Bibliothek im UI (eigener Tab), pro Domain zuweisbar, Built-in-OWASP-Plugins bleiben read-only + als Vorlage klonbar. Nur reine Rule-IDs/Ranges (keine SecLang-Ausführung, injektionssicher). - Migration 0047: Tabelle waf_app_profiles (repliziert via reconcile) + waf_configs.app_profiles. - Service/Handler: CRUD (/waf/profiles), Built-ins geschützt (builtin=false-Gate). - Agent-Loader: app_profiles → in effektive rule_exclusions gemerged; ihr updated_at hebt das effektive updated_at der Domain → Engine-Rebuild bei Profil-Edit. - UI: Profile-Tab (Liste/Editor mit durchsuchbaren Rule-IDs) + Multi-Select im Domain-Drawer. FIX (wichtig): ListAllWithDomain — der EINZIGE Loader des laufenden WAF-Agents — selektierte crs_plugins nie. Dadurch war cfg.CRSPlugins im Agent immer leer und KEIN Built-in-CRS-Plugin (Nextcloud/WordPress/Drupal) wurde je in die Engine inkludiert. Jetzt geladen (+ app_profiles). Die per-Domain-Plugin-Wahl wirkt damit erstmals tatsächlich. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
package waf
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
|
)
|
|
|
|
// ErrProfileNotFound wird von Get/Update/Delete zurückgegeben, wenn kein Profil
|
|
// mit der ID existiert.
|
|
var ErrProfileNotFound = errors.New("waf app profile not found")
|
|
|
|
const profileSelect = `
|
|
SELECT id, name, description, rule_exclusions, builtin, created_at, updated_at
|
|
FROM waf_app_profiles
|
|
`
|
|
|
|
func scanProfile(row pgx.Row) (*models.WafAppProfile, error) {
|
|
var p models.WafAppProfile
|
|
if err := row.Scan(&p.ID, &p.Name, &p.Description, &p.RuleExclusions, &p.Builtin, &p.CreatedAt, &p.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
// ListProfiles gibt alle App-Profile zurück (Built-in zuerst, dann alphabetisch).
|
|
func (r *Repo) ListProfiles(ctx context.Context) ([]models.WafAppProfile, error) {
|
|
rows, err := r.Pool.Query(ctx, profileSelect+" ORDER BY builtin DESC, name ASC")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]models.WafAppProfile, 0, 16)
|
|
for rows.Next() {
|
|
p, err := scanProfile(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// profilesByName lädt alle Profile in eine Name→Profil-Map (für die Auflösung
|
|
// im Agent-Loader).
|
|
func (r *Repo) profilesByName(ctx context.Context) (map[string]models.WafAppProfile, error) {
|
|
list, err := r.ListProfiles(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m := make(map[string]models.WafAppProfile, len(list))
|
|
for _, p := range list {
|
|
m[p.Name] = p
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// GetProfile gibt ein Profil per ID zurück, oder ErrProfileNotFound.
|
|
func (r *Repo) GetProfile(ctx context.Context, id int64) (*models.WafAppProfile, error) {
|
|
row := r.Pool.QueryRow(ctx, profileSelect+" WHERE id = $1", id)
|
|
p, err := scanProfile(row)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrProfileNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// CreateProfile legt ein neues benutzerdefiniertes Profil an (builtin immer
|
|
// false — Built-ins werden nicht über die API erzeugt).
|
|
func (r *Repo) CreateProfile(ctx context.Context, name, description string, exclusions []string) (*models.WafAppProfile, error) {
|
|
if exclusions == nil {
|
|
exclusions = []string{}
|
|
}
|
|
now := time.Now()
|
|
row := r.Pool.QueryRow(ctx, `
|
|
INSERT INTO waf_app_profiles (name, description, rule_exclusions, builtin, created_at, updated_at)
|
|
VALUES ($1,$2,$3,false,$4,$4)
|
|
RETURNING id, name, description, rule_exclusions, builtin, created_at, updated_at
|
|
`, name, description, exclusions, now)
|
|
return scanProfile(row)
|
|
}
|
|
|
|
// UpdateProfile ändert Name/Beschreibung/Ausnahmen eines Profils. Built-in-
|
|
// Profile sind read-only (WHERE builtin = false) → ErrProfileNotFound, wenn
|
|
// das Profil fehlt ODER built-in ist.
|
|
func (r *Repo) UpdateProfile(ctx context.Context, id int64, name, description string, exclusions []string) (*models.WafAppProfile, error) {
|
|
if exclusions == nil {
|
|
exclusions = []string{}
|
|
}
|
|
row := r.Pool.QueryRow(ctx, `
|
|
UPDATE waf_app_profiles
|
|
SET name = $2, description = $3, rule_exclusions = $4, updated_at = $5
|
|
WHERE id = $1 AND builtin = false
|
|
RETURNING id, name, description, rule_exclusions, builtin, created_at, updated_at
|
|
`, id, name, description, exclusions, time.Now())
|
|
p, err := scanProfile(row)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrProfileNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// DeleteProfile entfernt ein benutzerdefiniertes Profil. Built-in-Profile sind
|
|
// geschützt. Gibt ErrProfileNotFound zurück, wenn nichts gelöscht wurde.
|
|
func (r *Repo) DeleteProfile(ctx context.Context, id int64) error {
|
|
tag, err := r.Pool.Exec(ctx, `DELETE FROM waf_app_profiles WHERE id = $1 AND builtin = false`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrProfileNotFound
|
|
}
|
|
return nil
|
|
}
|