diff --git a/docs/features.md b/docs/features.md
index 2d55e702..741f2774 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -6,7 +6,8 @@ Here's everything Charon can do for you, explained simply.
## \u2699\ufe0f Optional Features
-Charon includes optional features that can be toggled on or off based on your needs. All features are enabled by default, giving you the full Charon experience from the start.
+Charon includes optional features that can be toggled on or off based on your needs.
+All features are enabled by default, giving you the full Charon experience from the start.
### What Are Optional Features?
@@ -19,12 +20,15 @@ Charon includes optional features that can be toggled on or off based on your ne
### Available Optional Features
#### Cerberus Security Suite
-- **What it is:** Complete security system including CrowdSec integration, country blocking, WAF protection, and access control
+
+- **What it is:** Complete security system including CrowdSec integration, country blocking,
+ WAF protection, and access control
- **When enabled:** Cerberus/Dashboard entries appear in the sidebar, all protection features are active
- **When disabled:** Security menu is hidden, all protection stops, but configuration data is preserved
- **Default:** Enabled
#### Uptime Monitoring
+
- **What it is:** Background checks that monitor if your websites are responding
- **When enabled:** Uptime menu appears in sidebar, automatic checks run every minute
- **When disabled:** Uptime menu is hidden, background checks stop, but uptime history is preserved
@@ -39,7 +43,9 @@ When you disable a feature:
- ✅ **API requests are blocked** — Feature-specific endpoints return appropriate errors
- ✅ **Configuration data is preserved** — Your settings remain intact if you re-enable the feature
-**Important:** Disabling a feature does NOT delete your data. All your security rules, uptime history, and configurations stay safe in the database. You can re-enable features at any time without losing anything.
+**Important:** Disabling a feature does NOT delete your data.
+All your security rules, uptime history, and configurations stay safe in the database.
+You can re-enable features at any time without losing anything.
### How to Toggle Features
@@ -59,6 +65,7 @@ When you disable a feature:
**Why you care:** Without it, browsers scream "NOT SECURE!" and people won't trust your site.
**What you do:** Nothing. Charon gets free certificates from Let's Encrypt and renews them automatically.
+
### Choose Your SSL Provider
**What it does:** Lets you select which Certificate Authority (CA) issues your SSL certificates.
@@ -69,21 +76,30 @@ When you disable a feature:
**Available options:**
-- **Auto (Recommended)** — The smart default. Tries Let's Encrypt first, automatically falls back to ZeroSSL if there are any issues. Best reliability with zero configuration.
+- **Auto (Recommended)** — The smart default. Tries Let's Encrypt first,
+ automatically falls back to ZeroSSL if there are any issues.
+ Best reliability with zero configuration.
-- **Let's Encrypt (Prod)** — Uses only Let's Encrypt production servers. Choose this if you specifically need Let's Encrypt certificates and have no rate limit concerns.
+- **Let's Encrypt (Prod)** — Uses only Let's Encrypt production servers.
+ Choose this if you specifically need Let's Encrypt certificates and have no rate limit concerns.
-- **Let's Encrypt (Staging)** — For testing purposes only. Issues certificates that browsers won't trust, but lets you test your configuration without hitting rate limits. See [Testing SSL Certificates](acme-staging.md) for details.
+- **Let's Encrypt (Staging)** — For testing purposes only.
+ Issues certificates that browsers won't trust, but lets you test your configuration without hitting rate limits.
+ See [Testing SSL Certificates](acme-staging.md) for details.
-- **ZeroSSL** — Uses only ZeroSSL as your certificate provider. Choose this if you prefer ZeroSSL or are hitting Let's Encrypt rate limits.
+- **ZeroSSL** — Uses only ZeroSSL as your certificate provider.
+ Choose this if you prefer ZeroSSL or are hitting Let's Encrypt rate limits.
-**Recommended setting:** Leave it on "Auto (Recommended)" unless you have a specific reason to change it. The auto mode gives you the best of both worlds—Let's Encrypt's speed with ZeroSSL as a backup.
+**Recommended setting:** Leave it on "Auto (Recommended)" unless you have a specific reason to change it.
+The auto mode gives you the best of both worlds—Let's Encrypt's speed with ZeroSSL as a backup.
**When to change it:**
+
- Testing configurations → Use "Let's Encrypt (Staging)"
- Hitting rate limits → Switch to "ZeroSSL"
- Specific CA requirement → Choose that specific provider
- Otherwise → Keep "Auto"
+
### Smart Certificate Cleanup
**What it does:** When you delete websites, Charon asks if you want to delete unused certificates too.
@@ -91,17 +107,20 @@ When you disable a feature:
**Why you care:** Custom and staging certificates can pile up over time. This helps you keep things tidy.
**How it works:**
+
- Delete a website → Charon checks if its certificate is used elsewhere
- If the certificate is custom or staging (not Let's Encrypt) and orphaned → you get a prompt
- Choose to keep or delete the certificate
- Default is "keep" (safe choice)
**When it prompts:**
+
- ✅ Custom certificates you uploaded
- ✅ Staging certificates (for testing)
- ❌ Let's Encrypt certificates (managed automatically)
**What you do:**
+
- See the prompt after clicking Delete on a proxy host
- Check the box if you want to delete the orphaned certificate
- Leave unchecked to keep the certificate (in case you need it later)
@@ -117,6 +136,7 @@ When you disable a feature:
**Why you care:** Know at a glance if any certificates need attention—expired, expiring soon, or still provisioning.
**What you see:**
+
- **Certificate Breakdown** — Visual count of certificates by status:
- ✅ Valid certificates (healthy, not expiring soon)
- ⚠️ Expiring certificates (within 30 days)
@@ -126,22 +146,26 @@ When you disable a feature:
- **Auto-Refresh** — Card automatically updates during certificate provisioning
**How it works:**
+
- The card polls for certificate status changes during active provisioning
- Progress bar shows visual feedback while Let's Encrypt/ZeroSSL issues certificates
- Once all certificates are ready, auto-refresh stops to save resources
-**What you do:** Check the Dashboard after adding new hosts to monitor certificate provisioning. If you see pending certificates, the system is working—just wait a moment for issuance to complete.
-
+**What you do:** Check the Dashboard after adding new hosts to monitor certificate provisioning.
+If you see pending certificates, the system is working—just wait a moment for issuance to complete.
---
## \ud83d\udee1\ufe0f Security (Optional)
-Charon includes **Cerberus**, a security system that blocks bad guys. It's off by default—turn it on when you're ready. The main page is the **Cerberus Dashboard** (sidebar: Cerberus → Dashboard).
+Charon includes **Cerberus**, a security system that blocks bad guys.
+It's off by default—turn it on when you're ready.
+The main page is the **Cerberus Dashboard** (sidebar: Cerberus → Dashboard).
### Block Bad IPs Automatically
-**What it does:** CrowdSec watches for attackers and blocks them before they can do damage. The overview now has a single Start/Stop toggle—no separate mode selector.
+**What it does:** CrowdSec watches for attackers and blocks them before they can do damage.
+The overview now has a single Start/Stop toggle—no separate mode selector.
**Why you care:** Someone tries to guess your password 100 times? Blocked automatically.
@@ -162,23 +186,29 @@ Charon includes **Cerberus**, a security system that blocks bad guys. It's off b
**Why you care:** Protects your apps even if they have bugs.
**What you do:** Turn on "WAF" mode in security settings.
+
### Zero-Day Exploit Protection
-**What it does:** The WAF (Web Application Firewall) can detect and block many zero-day exploits before they reach your apps.
+**What it does:** The WAF (Web Application Firewall) can detect and block many zero-day exploits
+before they reach your apps.
-**Why you care:** Even if a brand-new vulnerability is discovered in your software, the WAF might catch it by recognizing the attack pattern.
+**Why you care:** Even if a brand-new vulnerability is discovered in your software, the WAF might
+catch it by recognizing the attack pattern.
**How it works:**
+
- Attackers use predictable patterns (SQL syntax, JavaScript tags, command injection)
- The WAF inspects every request for these patterns
- If detected, the request is blocked or logged (depending on mode)
**What you do:**
+
1. Enable WAF in "Monitor" mode first (logs only, doesn't block)
2. Review logs for false positives
3. Switch to "Block" mode when ready
**Limitations:**
+
- Only protects web-based exploits (HTTP/HTTPS traffic)
- Does NOT protect against zero-days in Docker, Linux, or Charon itself
- Does NOT replace regular security updates
@@ -189,7 +219,8 @@ Charon includes **Cerberus**, a security system that blocks bad guys. It's off b
**What it does:** Connects your Charon instance to the global CrowdSec network to share and receive threat intelligence.
-**Why you care:** Protects your server from IPs that are attacking other people, and lets you manage your security configuration easily.
+**Why you care:** Protects your server from IPs that are attacking other people,
+and lets you manage your security configuration easily.
**Features:**
@@ -201,11 +232,13 @@ Charon includes **Cerberus**, a security system that blocks bad guys. It's off b
- **Console Enrollment:** Connect your instance to the CrowdSec Console web interface.
- **Visual Dashboard:** See your alerts and decisions in a beautiful cloud dashboard.
- - **Easy Setup:** Just click "Enroll" and paste your enrollment key. Charon automatically handles CAPI registration if needed.
+ - **Easy Setup:** Just click "Enroll" and paste your enrollment key.
+ Charon automatically handles CAPI registration if needed.
- **Configuration:** Uses the local configuration in `data/crowdsec/config.yaml` instead of system defaults.
- **Secure:** The enrollment key is passed securely to the CrowdSec agent.
- **Live Decisions:** See exactly who is being blocked and why in real-time.
+
---
## \ud83d\udc33 Docker Integration
@@ -245,13 +278,16 @@ Charon includes **Cerberus**, a security system that blocks bad guys. It's off b
**Why you care:** Know immediately how your import went—no guessing or digging through logs.
**What you see:**
+
- **Hosts Created** — New proxy hosts that were added to your configuration
- **Hosts Updated** — Existing hosts that were modified with new settings
- **Hosts Skipped** — Entries that weren't imported (duplicates or unsupported)
- **Certificate Guidance** — Instructions for SSL certificate provisioning
- **Quick Navigation** — Buttons to go directly to Dashboard or Proxy Hosts
-**What you do:** Review the summary after each import. If hosts were skipped, check the details to understand why. Use the navigation buttons to proceed with your workflow.
+**What you do:** Review the summary after each import.
+If hosts were skipped, check the details to understand why.
+Use the navigation buttons to proceed with your workflow.
---
@@ -271,17 +307,22 @@ When you make changes, Charon shows you themed animations so you know what's hap
### The Gold Coin (Login)
-When you log in, you see a spinning gold coin. In Greek mythology, people paid Charon the ferryman with a coin to cross the river into the afterlife. So logging in = paying for passage!
+When you log in, you see a spinning gold coin.
+In Greek mythology, people paid Charon the ferryman with a coin to cross the river into the afterlife.
+So logging in = paying for passage!
### The Blue Boat (Managing Websites)
-When you create or update websites, you see Charon's boat sailing across the river. He's literally "ferrying" your changes to the server.
+When you create or update websites, you see Charon's boat sailing across the river.
+He's literally "ferrying" your changes to the server.
### The Red Guardian (Security)
-When you change security settings, you see Cerberus—the three-headed guard dog. He protects the gates of the underworld, just like your security settings protect your apps.
+When you change security settings, you see Cerberus—the three-headed guard dog.
+He protects the gates of the underworld, just like your security settings protect your apps.
-**Why these exist:** Changes can take 1-10 seconds to apply. The animations tell you what's happening so you don't think it's broken.
+**Why these exist:** Changes can take 1-10 seconds to apply.
+The animations tell you what's happening so you don't think it's broken.
---
@@ -303,7 +344,8 @@ When you change security settings, you see Cerberus—the three-headed guard dog
**What you do:** View the "Uptime" page in the sidebar. Uptime checks run automatically in the background.
-**Optional:** You can disable this feature in System Settings → Optional Features if you don't need it. Your uptime history will be preserved.
+**Optional:** You can disable this feature in System Settings → Optional Features if you don't need it.
+Your uptime history will be preserved.
---
@@ -316,17 +358,21 @@ When you change security settings, you see Cerberus—the three-headed guard dog
**What you do:** Click "Logs" in the sidebar.
---
+
## 🔴 Live Security Logs & Notifications
**What it does:** Stream security events in real-time and get notified about critical threats.
-**Why you care:** See attacks as they happen, not hours later. Configure alerts for WAF blocks, ACL denials, and suspicious activity.
+**Why you care:** See attacks as they happen, not hours later.
+Configure alerts for WAF blocks, ACL denials, and suspicious activity.
### Live Log Viewer
-**Real-time streaming:** Watch security events appear instantly in the Cerberus Dashboard. Uses WebSocket technology to stream logs with zero delay.
+**Real-time streaming:** Watch security events appear instantly in the Cerberus Dashboard.
+Uses WebSocket technology to stream logs with zero delay.
**What you see:**
+
- WAF blocks (SQL injection attempts, XSS attacks, etc.)
- CrowdSec decisions (blocked IPs and why)
- Access control denials (geo-blocking, IP filtering)
@@ -334,6 +380,7 @@ When you change security settings, you see Cerberus—the three-headed guard dog
- All security-related events with full context
**Controls:**
+
- **Pause/Resume** — Stop the stream to examine specific entries
- **Clear** — Remove old entries to focus on new activity
- **Auto-scroll** — Automatically follows new entries (disable to scroll back)
@@ -342,6 +389,7 @@ When you change security settings, you see Cerberus—the three-headed guard dog
**Where to find it:** Cerberus → Dashboard → Live Activity section (bottom of page)
**Query parameters:** The WebSocket endpoint supports server-side filtering:
+
- `?level=error` — Only error-level logs
- `?source=waf` — Only WAF-related events
- `?source=cerberus` — All Cerberus security events
@@ -353,6 +401,7 @@ When you change security settings, you see Cerberus—the three-headed guard dog
**Where to configure:** Cerberus Dashboard → "Notification Settings" button (top-right)
**Settings:**
+
- **Enable/Disable** — Master toggle for all notifications
- **Minimum Log Level** — Only notify for warnings and errors (ignore info/debug)
- **Event Types:**
@@ -363,11 +412,13 @@ When you change security settings, you see Cerberus—the three-headed guard dog
- **Email Recipients** — Comma-separated list of email addresses
**Example use cases:**
+
- Get a Slack message when your site is under attack
- Email yourself when ACL rules block legitimate traffic (false positive alert)
- Send all WAF blocks to your SIEM system for analysis
**What you do:**
+
1. Go to Cerberus Dashboard
2. Click "Notification Settings"
3. Enable notifications
@@ -377,12 +428,14 @@ When you change security settings, you see Cerberus—the three-headed guard dog
7. Save
**Technical details:**
+
- Notifications respect the minimum log level (e.g., only send errors)
- Webhook payloads include full event context (IP, request details, rule matched)
- Email delivery requires SMTP configuration (future feature)
- Webhook retries with exponential backoff on failure
---
+
## \ud83d\udcbe Backup & Restore
**What it does:** Saves a copy of your configuration before destructive changes.
diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md
index 029867f6..268fdc66 100644
--- a/docs/plans/current_spec.md
+++ b/docs/plans/current_spec.md
@@ -1,784 +1,219 @@
-# CrowdSec Preset Apply Failure - Fix Plan
+# Implementation Plan: Rename WAF Card to Coraza on Cerberus Dashboard
-**Date:** December 12, 2025
-**Status:** Analysis Complete - Ready for Implementation
-**Severity:** High
+## Overview
----
+Modify the WAF card on the Cerberus Dashboard to:
-## Issue Summary
+1. Rename "WAF" to "Coraza" (consistent with how CrowdSec is named - the card uses the product name)
+2. Remove the Mode and Rule Set dropdowns from the card (these are handled on the config page)
-User reported error when applying a CrowdSec preset:
+## Reference: CrowdSec Card Pattern
-```
-Apply failed: read archive: open /app/data/crowdsec/hub_cache/crowdsecurity/caddy/bundle.tgz: no such file or directory. Backup created at /app/data/crowdsec.backup.20251211-194408
-```
+The CrowdSec card naming convention shows that we use the **product name** (CrowdSec) rather than generic terms. Similarly, WAF should become "Coraza" since Coraza is the underlying product.
----
+**CrowdSec Card Structure:**
-## Root Cause Analysis
+- Title: "CrowdSec" (not "IPS" or "Intrusion Prevention System")
+- Simple enabled/disabled status
+- Config button navigates to `/security/crowdsec` for detailed configuration
-### The Bug
+**Current WAF Card Issues:**
-The `Apply()` function in [hub_sync.go](../../backend/internal/crowdsec/hub_sync.go#L535) has a **fatal ordering bug** that destroys the cache before reading from it.
-
-### Detailed Flow
-
-1. **Pull Phase (Works Correctly)**
- - User pulls preset `crowdsecurity/caddy`
- - `HubCache.Store()` writes to: `/app/data/crowdsec/hub_cache/crowdsecurity/caddy/bundle.tgz`
- - `CachedPreset.ArchivePath` stores this absolute path
-
-2. **Apply Phase (Bug Occurs)**
- ```
- Step 1: loadCacheMeta() → Returns meta.ArchivePath = "/app/data/crowdsec/hub_cache/.../bundle.tgz"
- Step 2: backupExisting() → RENAMES "/app/data/crowdsec" to "/app/data/crowdsec.backup.TIMESTAMP"
- ⚠️ THIS MOVES THE CACHE TOO! hub_cache is INSIDE crowdsec/
- Step 3: cscli fails (not available or preset not in hub)
- Step 4: os.ReadFile(meta.ArchivePath) → FILE NOT FOUND!
- The path still points to "/app/data/crowdsec/..." but that directory was renamed!
- ```
-
-### Visual Representation
-
-**Before Backup:**
-```
-/app/data/crowdsec/
-├── hub_cache/
-│ └── crowdsecurity/
-│ └── caddy/
-│ ├── bundle.tgz ← meta.ArchivePath points here
-│ ├── preview.yaml
-│ └── metadata.json
-├── config.yaml
-└── other_files/
-```
-
-**After `backupExisting()` (line 535):**
-```
-/app/data/crowdsec.backup.20251211-194408/ ← Renamed!
-├── hub_cache/
-│ └── crowdsecurity/
-│ └── caddy/
-│ ├── bundle.tgz ← File is now HERE
-│ ├── preview.yaml
-│ └── metadata.json
-├── config.yaml
-└── other_files/
-
-/app/data/crowdsec/ ← Directory no longer exists!
-```
-
-**Result:** `os.ReadFile(meta.ArchivePath)` fails because the path `/app/data/crowdsec/hub_cache/.../bundle.tgz` no longer exists.
-
----
-
-## Why This Wasn't Caught Earlier
-
-1. **Tests use temp directories** - Each test creates fresh directories, so the race condition doesn't manifest
-2. **cscli path succeeds in CI** - When `cscli` is available and works, the code returns early before hitting the bug
-3. **Recent changes to backup logic** - The copy-based fallback and backup improvements may have introduced this ordering issue
-4. **Cache directory nested inside DataDir** - The architecture decision to put `hub_cache` inside `DataDir` (crowdsec config) creates this coupling
-
----
-
-## Fix Options
-
-### Option A: Read Archive Before Backup (Recommended)
-
-**Rationale:** Simple, minimal change, maintains existing backup behavior.
-
-**File:** [backend/internal/crowdsec/hub_sync.go](../../backend/internal/crowdsec/hub_sync.go)
-
-**Changes:**
-
-```go
-func (s *HubService) Apply(ctx context.Context, slug string) (ApplyResult, error) {
- // ... existing validation code ...
-
- result := ApplyResult{AppliedPreset: cleanSlug, Status: "failed"}
- meta, metaErr := s.loadCacheMeta(applyCtx, cleanSlug)
- if metaErr == nil {
- result.CacheKey = meta.CacheKey
- }
- hasCS := s.hasCSCLI(applyCtx)
-
- // === NEW: Read archive BEFORE backup ===
- var archive []byte
- var archiveErr error
- if metaErr == nil {
- archive, archiveErr = os.ReadFile(meta.ArchivePath)
- if archiveErr != nil {
- logger.Log().WithError(archiveErr).WithField("archive_path", meta.ArchivePath).Warn("failed to read cached archive before backup")
- }
- }
- // === END NEW ===
-
- backupPath := filepath.Clean(s.DataDir) + ".backup." + time.Now().Format("20060102-150405")
- if err := s.backupExisting(backupPath); err != nil {
- return result, fmt.Errorf("backup: %w", err)
- }
- result.BackupPath = backupPath
-
- // Try cscli first
- if hasCS {
- cscliErr := s.runCSCLI(applyCtx, cleanSlug)
- if cscliErr == nil {
- result.Status = "applied"
- result.ReloadHint = true
- result.UsedCSCLI = true
- return result, nil
- }
- logger.Log().WithField("slug", cleanSlug).WithError(cscliErr).Warn("cscli install failed; attempting cache fallback")
- }
-
- // === MODIFIED: Use pre-loaded archive or refresh ===
- if metaErr != nil || archiveErr != nil {
- refreshed, refreshErr := s.refreshCache(applyCtx, cleanSlug, metaErr)
- if refreshErr != nil {
- _ = s.rollback(backupPath)
- return result, fmt.Errorf("load cache for %s: %w", cleanSlug, refreshErr)
- }
- meta = refreshed
- result.CacheKey = meta.CacheKey
- // Re-read archive from refreshed cache location
- archive, archiveErr = os.ReadFile(meta.ArchivePath)
- if archiveErr != nil {
- _ = s.rollback(backupPath)
- return result, fmt.Errorf("read archive: %w", archiveErr)
- }
- }
-
- // Use the pre-loaded archive bytes
- if err := s.extractTarGz(applyCtx, archive, s.DataDir); err != nil {
- _ = s.rollback(backupPath)
- return result, fmt.Errorf("extract: %w", err)
- }
- // === END MODIFIED ===
-
- result.Status = "applied"
- result.ReloadHint = true
- result.UsedCSCLI = false
- return result, nil
-}
-```
-
-### Option B: Move Cache Outside DataDir
-
-**Rationale:** Architectural fix - separates transient cache from operational config.
-
-**Files to modify:**
-- [backend/internal/api/handlers/crowdsec_handler.go](../../backend/internal/api/handlers/crowdsec_handler.go) - Change cache location
-- [backend/internal/crowdsec/hub_sync.go](../../backend/internal/crowdsec/hub_sync.go) - Add cache dir parameter
-
-**Changes:**
-```go
-// In NewCrowdsecHandler:
-// BEFORE:
-cacheDir := filepath.Join(dataDir, "hub_cache")
-
-// AFTER:
-cacheDir := filepath.Join(filepath.Dir(dataDir), "hub_cache")
-// Results in: /app/data/hub_cache (sibling of crowdsec, not child)
-```
-
-**Pros:** Clean separation, cache survives config resets
-**Cons:** Breaking change for existing installs, requires migration
-
-### Option C: Selective Backup (Exclude Cache)
-
-**Rationale:** Only backup config files, not cache.
-
-**Changes to `backupExisting()`:**
-```go
-func (s *HubService) backupExisting(backupPath string) error {
- // ... existing checks ...
-
- // Skip hub_cache during backup - it's transient
- return filepath.WalkDir(s.DataDir, func(path string, d fs.DirEntry, err error) error {
- if strings.Contains(path, "hub_cache") {
- return filepath.SkipDir
- }
- // ... copy logic ...
- })
-}
-```
-
-**Pros:** Faster backups, cache preserved
-**Cons:** More complex, backup is no longer complete snapshot
-
----
-
-## Recommended Implementation
-
-**Choose Option A** for these reasons:
-
-1. **Minimal code change** - Single function modification
-2. **No breaking changes** - Existing cache paths remain valid
-3. **No migration needed** - Works immediately
-4. **Maintains complete backups** - Backup still captures full state
-5. **Easy to test** - Clear before/after behavior
-
----
+- Title: "WAF (Coraza)" - should just be "Coraza"
+- Contains Mode and Rule Set dropdowns (should be on config page only)
+- Inconsistent with CrowdSec card simplicity
## Files to Modify
-| File | Change |
-|------|--------|
-| [backend/internal/crowdsec/hub_sync.go](../../backend/internal/crowdsec/hub_sync.go) | Reorder archive read before backup in `Apply()` |
-| [backend/internal/crowdsec/hub_sync_test.go](../../backend/internal/crowdsec/hub_sync_test.go) | Add test for apply with backup scenario |
-| [backend/internal/crowdsec/hub_pull_apply_test.go](../../backend/internal/crowdsec/hub_pull_apply_test.go) | Add regression test |
+### 1. Frontend: Security Dashboard Page
----
+**File:** `frontend/src/pages/Security.tsx`
-## Specific Code Changes
+**Changes Required:**
-### Change 1: hub_sync.go - Apply() Function
+| Line(s) | Current Text/Code | New Text/Code | Notes |
+|---------|------------------|---------------|-------|
+| 171 | `Cerberus powers CrowdSec, WAF, ACLs, and Rate Limiting.` | `Cerberus powers CrowdSec, Coraza, ACLs, and Rate Limiting.` | Info banner text |
+| 317 | `{/* WAF - Layer 3: Request Inspection */}` | `{/* Coraza - Layer 3: Request Inspection */}` | Comment update |
+| 321 | `
WAF (Coraza)
` | `
Coraza
` | Card title |
+| 337-340 | Protection text using "WAF" terminology | Keep as-is | Still valid for "Coraza" |
+| **341-379** | **WAF Mode and Rule Set dropdowns (entire block)** | **REMOVE** | Delete the entire conditional block that renders dropdowns when WAF is enabled |
+| 383 | `onClick={() => navigate('/security/waf')}` | Keep same path | Route stays the same, just config page |
+| 385 | `{status.waf.enabled ? 'Manage Rule Sets' : 'Configure'}` | `{status.waf.enabled ? 'Configure' : 'Configure'}` or just `Configure` | Simplify button text |
-**Location:** Lines 514-580
+**Specific Block to Remove (Lines ~341-379):**
-**Before:**
-```go
-func (s *HubService) Apply(ctx context.Context, slug string) (ApplyResult, error) {
- cleanSlug := sanitizeSlug(slug)
- // ... validation ...
-
- result := ApplyResult{AppliedPreset: cleanSlug, Status: "failed"}
- meta, metaErr := s.loadCacheMeta(applyCtx, cleanSlug)
- if metaErr == nil {
- result.CacheKey = meta.CacheKey
- }
- hasCS := s.hasCSCLI(applyCtx)
-
- backupPath := filepath.Clean(s.DataDir) + ".backup." + time.Now().Format("20060102-150405")
- if err := s.backupExisting(backupPath); err != nil {
- return result, fmt.Errorf("backup: %w", err)
- }
- result.BackupPath = backupPath
-
- // Try cscli first
- if hasCS {
- // ... cscli logic ...
- }
-
- if metaErr != nil {
- // ... refresh cache logic ...
- }
-
- archive, err := os.ReadFile(meta.ArchivePath) // ❌ FAILS - file moved by backup!
- if err != nil {
- _ = s.rollback(backupPath)
- return result, fmt.Errorf("read archive: %w", err)
- }
- // ...
-}
-```
-
-**After:**
-```go
-func (s *HubService) Apply(ctx context.Context, slug string) (ApplyResult, error) {
- cleanSlug := sanitizeSlug(slug)
- // ... validation ...
-
- result := ApplyResult{AppliedPreset: cleanSlug, Status: "failed"}
- meta, metaErr := s.loadCacheMeta(applyCtx, cleanSlug)
- if metaErr == nil {
- result.CacheKey = meta.CacheKey
- }
- hasCS := s.hasCSCLI(applyCtx)
-
- // ✅ NEW: Read archive into memory BEFORE backup moves the files
- var archive []byte
- var archiveReadErr error
- if metaErr == nil {
- archive, archiveReadErr = os.ReadFile(meta.ArchivePath)
- if archiveReadErr != nil {
- logger.Log().WithError(archiveReadErr).WithField("archive_path", meta.ArchivePath).
- Warn("failed to read cached archive before backup")
- }
- }
-
- backupPath := filepath.Clean(s.DataDir) + ".backup." + time.Now().Format("20060102-150405")
- if err := s.backupExisting(backupPath); err != nil {
- return result, fmt.Errorf("backup: %w", err)
- }
- result.BackupPath = backupPath
-
- // Try cscli first
- if hasCS {
- cscliErr := s.runCSCLI(applyCtx, cleanSlug)
- if cscliErr == nil {
- result.Status = "applied"
- result.ReloadHint = true
- result.UsedCSCLI = true
- return result, nil
- }
- logger.Log().WithField("slug", cleanSlug).WithError(cscliErr).
- Warn("cscli install failed; attempting cache fallback")
- }
-
- // ✅ MODIFIED: Handle cache miss OR failed archive read
- if metaErr != nil || archiveReadErr != nil {
- // Need to refresh cache (either wasn't cached or file was unreadable)
- originalErr := metaErr
- if originalErr == nil {
- originalErr = archiveReadErr
- }
- refreshed, refreshErr := s.refreshCache(applyCtx, cleanSlug, originalErr)
- if refreshErr != nil {
- _ = s.rollback(backupPath)
- logger.Log().WithError(refreshErr).WithField("slug", cleanSlug).
- WithField("backup_path", backupPath).
- Warn("cache refresh failed; rolled back backup")
- result.ErrorMessage = fmt.Sprintf("load cache for %s: %v", cleanSlug, refreshErr)
- return result, fmt.Errorf("load cache for %s: %w", cleanSlug, refreshErr)
- }
- meta = refreshed
- result.CacheKey = meta.CacheKey
-
- // Read from the newly refreshed cache
- archive, archiveReadErr = os.ReadFile(meta.ArchivePath)
- if archiveReadErr != nil {
- _ = s.rollback(backupPath)
- return result, fmt.Errorf("read archive after refresh: %w", archiveReadErr)
- }
- }
-
- // ✅ Use pre-loaded archive bytes (no file read here)
- if err := s.extractTarGz(applyCtx, archive, s.DataDir); err != nil {
- _ = s.rollback(backupPath)
- return result, fmt.Errorf("extract: %w", err)
- }
-
- result.Status = "applied"
- result.ReloadHint = true
- result.UsedCSCLI = false
- return result, nil
-}
-```
-
-### Change 2: Add Regression Test
-
-**File:** [backend/internal/crowdsec/hub_pull_apply_test.go](../../backend/internal/crowdsec/hub_pull_apply_test.go)
-
-**New test:**
-```go
-func TestApplyReadsArchiveBeforeBackup(t *testing.T) {
- // This test verifies the fix for the bug where Apply() would:
- // 1. Load cache metadata (getting archive path)
- // 2. Backup DataDir (moving the cache!)
- // 3. Try to read archive from original path (FAIL!)
-
- baseDir := t.TempDir()
- dataDir := filepath.Join(baseDir, "crowdsec")
- cacheDir := filepath.Join(dataDir, "hub_cache")
-
- // Create cache
- cache, err := NewHubCache(cacheDir, time.Hour)
- require.NoError(t, err)
-
- // Create a mock hub server
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if strings.Contains(r.URL.Path, ".tgz") {
- // Return a valid tar.gz
- var buf bytes.Buffer
- gw := gzip.NewWriter(&buf)
- tw := tar.NewWriter(gw)
- content := []byte("test: value\n")
- tw.WriteHeader(&tar.Header{Name: "test.yaml", Size: int64(len(content)), Mode: 0644})
- tw.Write(content)
- tw.Close()
- gw.Close()
- w.Write(buf.Bytes())
- return
- }
- if strings.Contains(r.URL.Path, ".yaml") {
- w.Write([]byte("preview: content"))
- return
- }
- // Index
- w.Write([]byte(`{"items":[{"name":"test/preset","version":"1.0"}]}`))
- }))
- defer server.Close()
-
- hub := &HubService{
- Cache: cache,
- DataDir: dataDir,
- HTTPClient: server.Client(),
- HubBaseURL: server.URL,
- MirrorBaseURL: server.URL,
- PullTimeout: 10 * time.Second,
- ApplyTimeout: 10 * time.Second,
- }
-
- ctx := context.Background()
-
- // Pull to populate cache
- _, err = hub.Pull(ctx, "test/preset")
- require.NoError(t, err, "pull should succeed")
-
- // Verify cache exists
- _, err = cache.Load(ctx, "test/preset")
- require.NoError(t, err, "cache should exist after pull")
-
- // Add some extra files to DataDir to make backup more realistic
- require.NoError(t, os.WriteFile(filepath.Join(dataDir, "config.yaml"), []byte("test: config"), 0644))
-
- // Apply - this should NOT fail with "read archive: no such file"
- result, err := hub.Apply(ctx, "test/preset")
- require.NoError(t, err, "apply should succeed - archive should be read before backup")
- assert.Equal(t, "applied", result.Status)
- assert.NotEmpty(t, result.BackupPath)
-
- // Verify backup was created
- _, err = os.Stat(result.BackupPath)
- assert.NoError(t, err, "backup should exist")
-}
-```
-
----
-
-## Edge Cases to Consider
-
-| Scenario | Current Behavior | Fixed Behavior |
-|----------|-----------------|----------------|
-| First-time apply (no cache) | Fails with cache miss | Attempts refresh, same behavior |
-| cscli available and works | Returns early, never hits bug | Same - returns early |
-| cscli fails, cache exists | **FAILS** - archive moved | Succeeds - archive pre-loaded |
-| Archive file corrupted | Fails on read | Same - fails on read, but before backup |
-| Network down during refresh | Fails | Same - fails with clear error |
-| Large archive (>25MB) | Limited by maxArchiveSize | Same - memory is fine for 25MB |
-| Concurrent applies | Potential race | Still potential race (separate issue) |
-
----
-
-## Testing Plan
-
-1. **Unit Tests**
- - [ ] `TestApplyReadsArchiveBeforeBackup` - New regression test
- - [ ] Existing `TestPullThenApplyFlow` should still pass
- - [ ] `TestApplyWithoutPullFails` should still pass
-
-2. **Integration Tests**
- - [ ] Manual test in Docker container
- - [ ] Pull preset via UI
- - [ ] Apply preset via UI
- - [ ] Verify no "read archive" error
-
-3. **Edge Case Tests**
- - [ ] Apply with expired cache (should refresh)
- - [ ] Apply with network failure (should error gracefully)
- - [ ] Apply with cscli available (should use cscli path)
-
----
-
-## Rollout Plan
-
-1. **Implement fix** in `hub_sync.go`
-2. **Add regression test** in `hub_pull_apply_test.go`
-3. **Run full test suite**: `go test ./...`
-4. **Run pre-commit**: `pre-commit run --all-files`
-5. **Build and test locally**: `docker build -t charon:local .`
-6. **Manual verification in container**
-7. **Commit with**: `fix: read archive before backup in CrowdSec preset apply`
-
----
-
-## Related Files Reference
-
-| File | Purpose |
-|------|---------|
-| [hub_sync.go](../../backend/internal/crowdsec/hub_sync.go) | HubService.Apply() - main fix location |
-| [hub_cache.go](../../backend/internal/crowdsec/hub_cache.go) | Cache storage, stores ArchivePath |
-| [crowdsec_handler.go](../../backend/internal/api/handlers/crowdsec_handler.go) | HTTP handler, initializes cache |
-| [routes.go](../../backend/internal/api/routes/routes.go) | Sets crowdsecDataDir from config |
-| [config.go](../../backend/internal/config/config.go) | CrowdSecConfigDir default |
-
----
-
-## Summary
-
-**Root Cause:** The `Apply()` function backs up the entire DataDir (which includes the cache) before reading the cached archive, resulting in a "file not found" error.
-
-**Fix:** Read the archive into memory before creating the backup.
-
-**Impact:** Low risk - the fix only changes the order of operations and doesn't affect the backup or extraction logic.
-
-**Effort:** ~30 minutes implementation + testing
-| 1 | Cerberus shows ON by default on first load (should be OFF) | High |
-| 2 | Cerberus dashboard header shows "disabled" even when enabled | Medium |
-| 3 | CrowdSec toggle auto-enables when Cerberus is enabled | Medium |
-| 4 | CrowdSec toggle unresponsive + Config button grayed out | High |
-
----
-
-## Root Cause Analysis
-
-### Issue 1: Cerberus Shows ON by Default
-
-**Root Cause:** The `feature_flags_handler.go` has a default value of `true` for all feature flags including `feature.cerberus.enabled`.
-
-**File:** [backend/internal/api/handlers/feature_flags_handler.go#L39-L42](../../backend/internal/api/handlers/feature_flags_handler.go#L39-L42)
-
-```go
-// Line 39-42
-for _, key := range defaultFlags {
- defaultVal := true // <-- THIS IS THE BUG
- if v, ok := defaultFlagValues[key]; ok {
- defaultVal = v
- }
-```
-
-**Problem:** The code sets `defaultVal := true` for all flags, then only overrides it if the key exists in `defaultFlagValues`. However, `feature.cerberus.enabled` is NOT in `defaultFlagValues`:
-
-```go
-// Line 29-31
-var defaultFlagValues = map[string]bool{
- "feature.crowdsec.console_enrollment": false,
-}
-```
-
-**Result:** On first load with an empty database, `feature.cerberus.enabled` defaults to `true` instead of `false`.
-
-**Additional Context:**
-- The [backend/internal/config/config.go#L60](../../backend/internal/config/config.go#L60) correctly defaults `CerberusEnabled` to `false`:
- ```go
- CerberusEnabled: getEnvAny("false", "CERBERUS_SECURITY_CERBERUS_ENABLED", ...) == "true"
- ```
-- However, the feature flags handler ignores this config and uses its own default.
-
----
-
-### Issue 2: Dashboard Header Shows "Disabled" Even When Enabled
-
-**Root Cause:** The header banner logic in `Security.tsx` checks `status.cerberus?.enabled` which comes from the security status API, but there's a **data source mismatch**.
-
-**Files:**
-- [frontend/src/pages/Security.tsx#L141-L153](../../frontend/src/pages/Security.tsx#L141-L153) - Header banner logic
-- [backend/internal/api/handlers/security_handler.go#L35-L49](../../backend/internal/api/handlers/security_handler.go#L35-L49) - Security status API
-
-**Problem Flow:**
-
-1. **Security.tsx** checks `status.cerberus?.enabled` from `/api/v1/security/status`
-2. **security_handler.go** reads from config AND settings table:
- ```go
- // Line 36-48
- enabled := h.cfg.CerberusEnabled
- var settingKey = "security.cerberus.enabled" // <-- WRONG KEY!
- if h.db != nil {
- var setting struct{ Value string }
- if err := h.db.Raw("SELECT value FROM settings WHERE key = ? LIMIT 1", settingKey).Scan(&setting).Error; ...
- ```
-3. **SystemSettings.tsx** toggles `feature.cerberus.enabled` (via feature flags API)
-
-**The Mismatch:**
-
-| Component | Key Used |
-|-----------|----------|
-| SystemSettings toggle | `feature.cerberus.enabled` |
-| Security status API | `security.cerberus.enabled` |
-
-The toggle writes to `feature.cerberus.enabled` but the security status reads from `security.cerberus.enabled` - **two different keys!**
-
----
-
-### Issue 3: CrowdSec Auto-Enables When Cerberus is Enabled
-
-**Root Cause:** The `docker-compose.override.yml` and `docker-compose.local.yml` both set `CHARON_SECURITY_CROWDSEC_MODE=local`:
-
-**File:** [docker-compose.override.yml#L21](../../docker-compose.override.yml#L21)
-```yaml
-- CHARON_SECURITY_CROWDSEC_MODE=local
-```
-
-**Problem:** When the container starts:
-1. Config loads with `CrowdSecMode: "local"` from env var
-2. Security status API returns `crowdsec.enabled: true` because mode is "local"
-3. Frontend shows CrowdSec as enabled
-
-**File:** [backend/internal/api/handlers/security_handler.go#L59-L62](../../backend/internal/api/handlers/security_handler.go#L59-L62)
-```go
-// Allow runtime override for CrowdSec enabled flag via settings table
-crowdsecEnabled := mode == "local" // <-- Auto-true if mode is "local"
-```
-
----
-
-### Issue 4: CrowdSec Toggle Unresponsive + Config Button Grayed Out
-
-**Root Cause:** Multiple issues combine to break the toggle:
-
-**A. Toggle Disabled Logic:**
-
-**File:** [frontend/src/pages/Security.tsx#L127](../../frontend/src/pages/Security.tsx#L127)
```tsx
-const crowdsecToggleDisabled = cerberusDisabled || crowdsecPowerMutation.isPending
+{status.waf.enabled && (
+
+)}
```
-**File:** [frontend/src/pages/Security.tsx#L126](../../frontend/src/pages/Security.tsx#L126)
-```tsx
-const cerberusDisabled = !status.cerberus?.enabled
-```
+### 2. Frontend: Layout Navigation
-Since `status.cerberus?.enabled` is `false` due to Issue 2 (wrong settings key), `cerberusDisabled` is `true`, making the toggle disabled.
+**File:** `frontend/src/components/Layout.tsx`
-**B. Config Button Disabled:**
+**Changes Required:**
-**File:** [frontend/src/pages/Security.tsx#L128](../../frontend/src/pages/Security.tsx#L128)
-```tsx
-const crowdsecControlsDisabled = cerberusDisabled || crowdsecPowerMutation.isPending
-```
+| Line | Current Text | New Text |
+|------|-------------|----------|
+| 70 | `{ name: 'WAF (Coraza)', path: '/security/waf', icon: '🛡️' }` | `{ name: 'Coraza', path: '/security/waf', icon: '🛡️' }` |
-Same logic - the controls are disabled because Cerberus appears disabled.
+### 3. Test Files to Update
-**C. Switch Component Event Handling:**
+#### 3.1 Security Page Tests
-**File:** [frontend/src/components/ui/Switch.tsx#L17-L20](../../frontend/src/components/ui/Switch.tsx#L17-L20)
+**File:** `frontend/src/pages/__tests__/Security.spec.tsx`
-The Switch component passes `disabled` to the native checkbox input, which prevents click events. This is correct behavior - the issue is the `disabled` prop is incorrectly `true`.
+**Changes Required:**
----
+| Test Name | Change Description |
+|-----------|-------------------|
+| `shows WAF mode selector when WAF is enabled` | **DELETE entire test** - Mode selector no longer on dashboard |
+| `shows WAF ruleset selector with available rulesets` | **DELETE entire test** - Ruleset selector no longer on dashboard |
+| `calls updateSecurityConfig when WAF mode is changed` | **DELETE entire test** - No mode selector on dashboard |
+| `calls updateSecurityConfig when WAF ruleset is changed` | **DELETE entire test** - No ruleset selector on dashboard |
+| `shows warning when no rulesets are configured` | **DELETE entire test** - Warning no longer on dashboard |
+| `displays correct WAF threat protection summary when enabled` | Keep but update any "WAF" string references if needed |
+| `does not show WAF controls when WAF is disabled` | **DELETE entire test** - Controls never shown on dashboard now |
-## Recommended Fixes
+**Tests to delete (Lines ~189-344):**
-### Fix 1: Update Feature Flag Defaults
+- `it('shows WAF mode selector when WAF is enabled', ...)`
+- `it('shows WAF ruleset selector with available rulesets', ...)`
+- `it('calls updateSecurityConfig when WAF mode is changed', ...)`
+- `it('calls updateSecurityConfig when WAF ruleset is changed', ...)`
+- `it('shows warning when no rulesets are configured', ...)`
+- `it('does not show WAF controls when WAF is disabled', ...)`
-**File:** `backend/internal/api/handlers/feature_flags_handler.go`
+Keep:
-```go
-// Change defaultFlagValues to include cerberus.enabled as false
-var defaultFlagValues = map[string]bool{
- "feature.cerberus.enabled": false, // ADD THIS
- "feature.crowdsec.console_enrollment": false,
- "feature.uptime.enabled": true, // Uptime can default ON
-}
-```
+- `it('displays correct WAF threat protection summary when enabled', ...)` - This tests the protection description text, not the dropdowns
-### Fix 2: Align Settings Keys
+#### 3.2 Security Audit Tests
-**Option A (Recommended):** Update security_handler.go to read from feature flags key
+**File:** `frontend/src/pages/__tests__/Security.audit.test.tsx`
-**File:** `backend/internal/api/handlers/security_handler.go`
+**Changes Required:**
-```go
-// Line 37: Change from
-var settingKey = "security.cerberus.enabled"
-// To
-var settingKey = "feature.cerberus.enabled"
-```
+| Line | Current Text | New Text |
+|------|-------------|----------|
+| 287 | `expect(screen.getByText('WAF (Coraza)')).toBeInTheDocument()` | `expect(screen.getByText('Coraza')).toBeInTheDocument()` |
+| 306-315 | `it('WAF controls have proper test IDs when enabled', ...)` | **DELETE entire test** - WAF controls no longer on dashboard |
+| 344 | `expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'WAF (Coraza)', 'Rate Limiting', 'Live Security Logs'])` | `expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'Coraza', 'Rate Limiting', 'Live Security Logs'])` |
-**Option B:** Create a sync mechanism between feature flags and security settings
+### 4. WAF Config Page (NO CHANGES NEEDED)
-### Fix 3: Remove CrowdSec Mode Override from Docker Compose
+**File:** `frontend/src/pages/WafConfig.tsx`
-**Files:**
-- `docker-compose.override.yml`
-- `docker-compose.local.yml`
+The WAF config page already properly uses "WAF" terminology in its title ("WAF Configuration") and references "Coraza" where appropriate. Since this is the configuration page, the Mode and Rule Set selections should remain here. **No changes required.**
-```yaml
-# Remove or comment out:
-# - CHARON_SECURITY_CROWDSEC_MODE=local
-# Or change to:
-- CHARON_SECURITY_CROWDSEC_MODE=disabled
-```
+### 5. WAF Config Tests (NO CHANGES NEEDED)
-### Fix 4: No Additional Fix Needed
+**File:** `frontend/src/pages/__tests__/WafConfig.spec.tsx`
-Issue 4 is a symptom of Issues 1-2. Once those are fixed:
-- `cerberusDisabled` will be `false` when Cerberus is enabled
-- `crowdsecToggleDisabled` will be `false`
-- `crowdsecControlsDisabled` will be `false`
-- Toggle and Config button will be interactive
+These tests test the WafConfig page which is unaffected. **No changes required.**
----
+## Summary of Changes
-## Test Scenarios
+| File | Change Type | Description |
+|------|-------------|-------------|
+| `frontend/src/pages/Security.tsx` | Modify | Rename "WAF" → "Coraza", remove Mode/RuleSet dropdowns |
+| `frontend/src/components/Layout.tsx` | Modify | Rename nav item "WAF (Coraza)" → "Coraza" |
+| `frontend/src/pages/__tests__/Security.spec.tsx` | Delete tests | Remove 6 tests for WAF dropdown controls |
+| `frontend/src/pages/__tests__/Security.audit.test.tsx` | Modify | Update card name assertions, remove dropdown test |
-### Test 1: Fresh Install Default State
-```
-Given: Clean database, no env vars set
-When: User loads the Settings > System page
-Then: Cerberus toggle should be OFF
-And: /api/v1/feature-flags returns { "feature.cerberus.enabled": false }
-```
+## API Changes
-### Test 2: Cerberus Toggle Sync
-```
-Given: User is on Settings > System page
-When: User enables Cerberus toggle
-Then: /api/v1/security/status returns { "cerberus": { "enabled": true } }
-And: Security dashboard header banner is NOT displayed
-```
+**None required.** This is purely a frontend UI change. The backend API endpoints, types, and data structures remain unchanged.
-### Test 3: CrowdSec Toggle Interaction
-```
-Given: Cerberus is enabled
-And: User is on Security dashboard
-When: User clicks CrowdSec toggle
-Then: Toggle should respond to click
-And: CrowdSec enabled state should change
-And: Toast notification should appear
-```
+## Type Definition Changes
-### Test 4: CrowdSec Config Button
-```
-Given: Cerberus is enabled
-And: User is on Security dashboard
-When: User clicks CrowdSec "Config" button
-Then: User should navigate to /security/crowdsec
-And: Button should NOT be grayed out
-```
+**None required.** The SecurityStatus type and related interfaces don't need modification.
-### Test 5: Environment Variable Override
-```
-Given: CERBERUS_SECURITY_CERBERUS_ENABLED=true set
-When: User loads Settings > System (fresh DB)
-Then: Cerberus toggle should be ON (env override)
-```
+## Text/Label Changes Summary
----
+| Location | From | To |
+|----------|------|-----|
+| Card title (Security.tsx) | `WAF (Coraza)` | `Coraza` |
+| Nav sidebar (Layout.tsx) | `WAF (Coraza)` | `Coraza` |
+| Info banner (Security.tsx) | `CrowdSec, WAF, ACLs` | `CrowdSec, Coraza, ACLs` |
+| Comment (Security.tsx) | `/* WAF - Layer 3 */` | `/* Coraza - Layer 3 */` |
+| Test assertions | `WAF (Coraza)` | `Coraza` |
-## Implementation Priority
+## Button Simplification
-| Priority | Fix | Effort | Impact |
-|----------|-----|--------|--------|
-| P0 | Fix 2 (Key alignment) | Low | High - Fixes Issues 2, 4 |
-| P1 | Fix 1 (Default values) | Low | High - Fixes Issue 1 |
-| P2 | Fix 3 (Docker compose) | Low | Medium - Fixes Issue 3 |
+The button text on the Coraza card should be simplified:
----
+- **Current:** `{status.waf.enabled ? 'Manage Rule Sets' : 'Configure'}`
+- **New:** `Configure` (always)
-## Files to Modify
+This matches the CrowdSec card pattern which just shows "Config" regardless of enabled state.
-1. **backend/internal/api/handlers/feature_flags_handler.go** - Add default value for cerberus
-2. **backend/internal/api/handlers/security_handler.go** - Change settings key to `feature.cerberus.enabled`
-3. **docker-compose.override.yml** - Remove or change CrowdSec mode
-4. **docker-compose.local.yml** - Remove or change CrowdSec mode
+## Implementation Order
----
+1. Update `Security.tsx`:
+ - Change card title from "WAF (Coraza)" to "Coraza"
+ - Update banner text
+ - Update comment
+ - Remove the dropdown controls block
+ - Simplify button text
-## Additional Observations
+2. Update `Layout.tsx`:
+ - Change nav item name
-1. **Dual Control Systems:** There are two overlapping control systems:
- - Feature flags (`feature.cerberus.enabled`) - toggled in SystemSettings.tsx
- - Security config (`SecurityConfig.Enabled` in DB) - used by Enable/Disable endpoints
+3. Update test files:
+ - `Security.spec.tsx`: Remove obsolete tests
+ - `Security.audit.test.tsx`: Update assertions, remove dropdown test
- Consider consolidating to one source of truth.
+4. Run tests to verify: `cd frontend && npm test`
-2. **Config vs Settings:** The `config.SecurityConfig` struct loaded from env vars is separate from DB-backed `SecurityConfig` model. This creates confusion about which takes precedence.
+5. Run type check: `cd frontend && npm run type-check`
-3. **No Migration:** When updating default values, existing users may need a migration or reset to see the new defaults.
+6. Run pre-commit checks
----
+## Verification Checklist
-## Code Reference Summary
-
-| File | Line | Purpose |
-|------|------|---------|
-| `feature_flags_handler.go` | L29-31 | Missing cerberus default |
-| `feature_flags_handler.go` | L39 | `defaultVal := true` bug |
-| `security_handler.go` | L37 | Wrong settings key |
-| `Security.tsx` | L126-128 | Disabled state logic |
-| `SystemSettings.tsx` | L99-105 | Feature toggle UI |
-| `docker-compose.override.yml` | L21 | CrowdSec mode env var |
-| `config.go` | L60 | Correct cerberus default |
+- [ ] Card title shows "Coraza" (not "WAF (Coraza)")
+- [ ] Nav sidebar shows "Coraza"
+- [ ] No Mode dropdown on dashboard card
+- [ ] No Rule Set dropdown on dashboard card
+- [ ] "Configure" button navigates to `/security/waf` config page
+- [ ] Mode/Rule Set controls still available on `/security/waf` config page
+- [ ] All tests pass
+- [ ] TypeScript compiles without errors
+- [ ] Pre-commit hooks pass
diff --git a/docs/reports/qa_report.md b/docs/reports/qa_report.md
index 2143f3e5..61d25af9 100644
--- a/docs/reports/qa_report.md
+++ b/docs/reports/qa_report.md
@@ -1,335 +1,135 @@
-# QA Security Audit Report
-
----
-
-## Cerberus Fixes Verification
+# QA Security Report: WAF to Coraza Rename
**Date:** December 12, 2025
-**QA Agent:** QA_Security
-**Status:** ✅ **PASS**
+**Agent:** QA_Security
+**Scope:** Frontend UI changes renaming "WAF (Coraza)" to "Coraza"
-### Test Summary
+---
-| Check | Result | Details |
+## Executive Summary
+
+**Overall Status: ✅ PASS**
+
+All tests pass after fixing test assertions to match the new UI. The rename from "WAF (Coraza)" to "Coraza" has been successfully implemented and verified.
+
+---
+
+## Test Results
+
+### TypeScript Compilation
+
+| Check | Status |
+|-------|--------|
+| `npm run type-check` | ✅ PASS |
+
+**Output:** Clean compilation with no errors.
+
+### Frontend Unit Tests
+
+| Metric | Count |
+|--------|-------|
+| Test Files | 84 |
+| Tests Passed | 728 |
+| Tests Skipped | 2 |
+| Tests Failed | 0 |
+| Duration | ~61s |
+
+**Initial Run:** 4 failures related to outdated test assertions
+**After Fix:** All 728 tests passing
+
+#### Issues Found and Fixed
+
+1. **Security.test.tsx - Line 281**
+ - **Issue:** Test expected card title `'WAF (Coraza)'` but UI shows `'Coraza'`
+ - **Severity:** Low (test sync issue)
+ - **Fix:** Updated assertion to expect `'Coraza'`
+
+2. **Security.test.tsx - Lines 252-267 (WAF Controls describe block)**
+ - **Issue:** Tests for `waf-mode-select` and `waf-ruleset-select` dropdowns that were removed from the Security page
+ - **Severity:** Low (removed UI elements)
+ - **Fix:** Removed the `WAF Controls` test suite as dropdowns are now on dedicated `/security/waf` page
+
+### Lint Results
+
+| Tool | Errors | Warnings |
+|------|--------|----------|
+| ESLint | 0 | 5 |
+
+**Warnings (pre-existing, not related to this change):**
+
+- `CrowdSecConfig.tsx:212` - React Hook useEffect missing dependencies
+- `CrowdSecConfig.tsx:715` - Unexpected any type
+- `CrowdSecConfig.spec.tsx:258,284,317` - Unexpected any types in tests
+
+### Pre-commit Hooks
+
+| Hook | Status |
+|------|--------|
+| Go Test Coverage (85.1%) | ✅ PASS |
+| Go Vet | ✅ PASS |
+| Check .version matches Git tag | ✅ PASS |
+| Prevent large files not tracked by LFS | ✅ PASS |
+| Prevent committing CodeQL DB artifacts | ✅ PASS |
+| Prevent committing data/backups files | ✅ PASS |
+| Frontend TypeScript Check | ✅ PASS |
+| Frontend Lint (Fix) | ✅ PASS |
+
+---
+
+## File Verification
+
+### Security.tsx (`frontend/src/pages/Security.tsx`)
+
+| Check | Status | Details |
|-------|--------|---------|
-| Backend Tests | ✅ PASS | All packages pass, 85.1% coverage (≥85% required) |
-| Frontend Tests | ⚠️ PASS* | 83/84 test files pass, 727/730 tests pass |
-| Frontend Build | ✅ PASS | Production build successful |
-| Pre-commit | ✅ PASS | All hooks pass |
+| Card title shows "Coraza" | ✅ Verified | Line 320: `
Coraza
` |
+| No "WAF (Coraza)" text in card title | ✅ Verified | Confirmed via grep search |
+| Dropdowns removed from Security page | ✅ Verified | Controls moved to `/security/waf` config page |
+| Internal API field names unchanged | ✅ Verified | `status.waf.enabled`, `toggle-waf` testid preserved for API compatibility |
-*Note: 1 flaky test in `LiveLogViewer.test.tsx` (WebSocket timing issue, not related to Cerberus)
+### Layout.tsx (`frontend/src/components/Layout.tsx`)
-### Issue Fix Verification
-
-#### Issue 1: Cerberus Default State in Feature Flags
-**File:** [feature_flags_handler.go](../../backend/internal/api/handlers/feature_flags_handler.go#L32)
-
-✅ **VERIFIED** - Line 32:
-```go
-"feature.cerberus.enabled": false, // Cerberus OFF by default
-```
-
-#### Issue 2: Security Handler Reads Correct Setting Key
-**File:** [security_handler.go](../../backend/internal/api/handlers/security_handler.go#L38)
-
-✅ **VERIFIED** - Line 38:
-```go
-var settingKey = "feature.cerberus.enabled"
-```
-
-The handler correctly reads from `feature.cerberus.enabled` (not an incorrect key).
-
-#### Issue 3: Docker Compose Files Have CrowdSec Disabled
-✅ **VERIFIED** - Found in:
-- `docker-compose.local.yml:25` - `CHARON_SECURITY_CROWDSEC_MODE=disabled`
-- `docker-compose.override.yml:25` - `CHARON_SECURITY_CROWDSEC_MODE=disabled`
-- `docker-compose.yml:25` - Commented template with `disabled` option
-- `docker-compose.dev.yml:25` - Commented template with `disabled` option
-
-### Cerberus Fixes Conclusion
-
-All three Cerberus-related fixes have been verified:
-
-1. ✅ Feature flags default `feature.cerberus.enabled` to `false`
-2. ✅ Security handler reads from correct setting key `feature.cerberus.enabled`
-3. ✅ Docker compose files set `CROWDSEC_MODE=disabled` in active configurations
-
-**Cerberus Verification: PASS**
+| Check | Status | Details |
+|-------|--------|---------|
+| Navigation shows "Coraza" | ✅ Verified | Line 70: `{ name: 'Coraza', path: '/security/waf', icon: '🛡️' }` |
---
-## Import Modal and Certificate Status Card Features
+## Changes Made During QA
-**Date:** December 11, 2025
-**Auditor:** QA_Security Agent
-**Overall Status:** ⚠️ **PARTIAL PASS**
+### Test File Update: Security.test.tsx
-### Executive Summary
+```diff
+- describe('WAF Controls', () => {
+- it('should change WAF mode', async () => { ... })
+- it('should change WAF ruleset', async () => { ... })
+- })
++ // Note: WAF Controls tests removed - dropdowns moved to dedicated WAF config page (/security/waf)
-The import modal (`ImportSuccessModal`) and certificate status card (`CertificateStatusCard`) features have been audited for code quality, type safety, accessibility, and proper testing. The core features are well-implemented with comprehensive test coverage, but there are **5 failing tests in CrowdSecConfig** (unrelated to the audited features) that need attention.
-
-### Test Results Summary
-
-### 1. TypeScript Type Check ✅ PASS
+- expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'WAF (Coraza)', 'Rate Limiting', 'Live Security Logs'])
++ expect(cardNames).toEqual(['CrowdSec', 'Access Control', 'Coraza', 'Rate Limiting', 'Live Security Logs'])
```
-npm run type-check - Passed
-No TypeScript errors detected
-```
-
-### 2. Frontend Tests with Coverage ⚠️ PARTIAL PASS
-```
-Test Files: 82 passed, 2 failed (84 total)
-Tests: 723 passed, 5 failed, 2 skipped (730 total)
-```
-
-**Failed Tests (in CrowdSecConfig - not related to audited features):**
-- `CrowdSecConfig.coverage.test.tsx`: 3 failures
- - `auto-selects first preset and pulls preview` - Element not found `preset-select`
- - `reads, edits, saves, and closes files` - Multiple textbox elements found
- - `shows overlay messaging for preset pull, apply, import, write, and mode updates` - Multiple textbox elements found
-- `CrowdSecConfig.spec.tsx`: 2 failures
- - `lists files, reads file content and can save edits` - Multiple textbox elements found
- - `disables apply and offers cached preview when hub is unavailable` - Element not found `preset-select`
-
-**ImportSuccessModal Tests:** ✅ All passing (12 tests)
-**CertificateStatusCard Tests:** ✅ All passing (14 tests)
-
-### 3. ESLint ✅ PASS (with warnings)
-```
-5 warnings (0 errors)
-```
-Warnings are in unrelated files (CrowdSecConfig.tsx):
-- Missing dependencies in useEffect hook
-- Explicit `any` types in test files
-
-### 4. Backend Build ✅ PASS
-```
-go build ./... - Passed
-No compilation errors
-```
-
-### 5. Backend Tests ✅ PASS
-```
-All packages: PASS
-Coverage: 85.1% (minimum required 85%)
-```
-
-### 6. Pre-commit ✅ PASS
-```
-All hooks passed:
-- Go Vet: Passed
-- Frontend TypeScript Check: Passed
-- Frontend Lint (Fix): Passed
-- Large file checks: Passed
-- CodeQL DB artifact checks: Passed
-```
-
----
-
-## Code Review: ImportSuccessModal
-
-### File: `frontend/src/components/dialogs/ImportSuccessModal.tsx`
-
-#### ✅ Strengths
-
-1. **Type Safety**
- - Well-defined `ImportSuccessModalProps` interface
- - Explicit typing for all props including `results` structure
- - Null safety with early return when `!visible || !results`
-
-2. **Error Handling**
- - Dedicated error section with proper conditional rendering
- - Scrollable error list with `max-h-24 overflow-y-auto`
- - Clear error count display with proper pluralization
-
-3. **Accessibility**
- - Backdrop click to close modal
- - Clear visual hierarchy with icons (CheckCircle, AlertCircle, Info)
- - Focus-visible button styles with `transition-colors`
-
-4. **Styling Consistency**
- - Uses project's design tokens (`bg-dark-card`, `bg-blue-active`)
- - Responsive layout with `flex-wrap` and `max-w-full mx-4`
- - Consistent spacing and color scheme
-
-5. **Memory/Cleanup**
- - No subscriptions or event listeners to clean up
- - Pure functional component with no side effects
-
-#### ⚠️ Recommendations
-
-1. **Accessibility Enhancement**
- - Add `role="dialog"` and `aria-modal="true"` to modal container
- - Add `aria-labelledby` pointing to title element
- - Consider focus trapping for keyboard navigation
-
-```tsx
-// Recommended enhancement:
-
-
- Import Completed
-
-```
-
-2. **Keyboard Support**
- - Add `onKeyDown` handler for Escape key to close modal
-
----
-
-## Code Review: CertificateStatusCard
-
-### File: `frontend/src/components/CertificateStatusCard.tsx`
-
-#### ✅ Strengths
-
-1. **Type Safety**
- - Uses imported `Certificate` and `ProxyHost` types
- - Clean interface definition for props
- - No `any` types used
-
-2. **Computed Values**
- - Efficient calculation of certificate status counts
- - Smart pending detection logic (SSL forced + enabled + no cert)
- - Progress percentage with edge case handling (empty array = 100%)
-
-3. **Accessibility**
- - Uses `Link` component for navigation (accessible by default)
- - Visible focus states inherited from router Link
-
-4. **Styling Consistency**
- - Follows card design pattern used elsewhere
- - Responsive hover transitions
- - Animated spinner for pending state (`animate-spin`)
-
-5. **Memory/Cleanup**
- - Stateless functional component
- - No subscriptions or event listeners
-
-#### ✅ No Issues Found
-
-The component is clean, well-typed, and follows best practices.
-
----
-
-## Code Review: useImport Hook
-
-### File: `frontend/src/hooks/useImport.ts`
-
-#### ✅ Strengths
-
-1. **State Management**
- - Proper use of `useState` for local state (`commitSucceeded`, `commitResult`)
- - Correct query invalidation patterns
- - Smart polling logic with `refetchInterval`
-
-2. **Error Handling**
- - Comprehensive error aggregation from multiple sources
- - Guards against 404 errors after commit (expected behavior)
- - Clear error message extraction
-
-3. **Memory/Cleanup**
- - React Query handles cleanup automatically
- - Proper cache removal with `removeQueries` on success/cancel
- - `clearCommitResult` function for state reset
-
-4. **Type Safety**
- - Explicit type imports
- - Type re-exports for consumers
-
----
-
-## Test Coverage Analysis
-
-### ImportSuccessModal.test.tsx ✅
-- **12 tests** covering all major functionality
-- Tests for rendering, user interactions, and edge cases
-- Proper mock setup with `vi.fn()`
-- Grammar tests (singular/plural)
-- Visibility/null result tests
-
-### CertificateStatusCard.test.tsx ✅
-- **14 tests** covering all states
-- Router wrapper setup correct
-- Progress calculation tests
-- Edge cases (empty arrays, disabled hosts, no SSL)
-- Link destination verification
-
----
-
-## Issues Found (Unrelated to Audited Features)
-
-### CrowdSecConfig Test Failures
-The failing tests are in `CrowdSecConfig.spec.tsx` and `CrowdSecConfig.coverage.test.tsx`. The issues are:
-
-1. **Element selection conflict**: Tests use `screen.getByRole('textbox')` but the component now has multiple textbox elements (search input + textarea)
-2. **Missing `preset-select` testid**: Some tests expect a `data-testid="preset-select"` element that may have been refactored
-
-**Recommendation**: Update CrowdSecConfig tests to use more specific selectors:
-```tsx
-// Instead of:
-const textarea = screen.getByRole('textbox')
-// Use:
-const textarea = screen.getByTestId('crowdsec-file-textarea')
-// Or:
-const textarea = screen.getAllByRole('textbox')[1] // if order is consistent
-```
-
----
-
-## Security Checklist
-
-| Check | Status |
-|-------|--------|
-| No hardcoded secrets | ✅ |
-| No console.log statements | ✅ |
-| Input sanitization (handled by React) | ✅ |
-| XSS prevention (React escapes by default) | ✅ |
-| No direct DOM manipulation | ✅ |
-| Proper error message display (no stack traces) | ✅ |
-
----
-
-## Final Assessment
-
-### Features Under Review
-| Component | Status | Notes |
-|-----------|--------|-------|
-| ImportSuccessModal | ✅ PASS | Well-implemented, minor a11y enhancement recommended |
-| CertificateStatusCard | ✅ PASS | Clean, no issues |
-| useImport Hook | ✅ PASS | Proper state management |
-
-### Overall Codebase
-| Check | Status |
-|-------|--------|
-| TypeScript | ✅ PASS |
-| ESLint | ✅ PASS (warnings only) |
-| Backend Build | ✅ PASS |
-| Backend Tests | ✅ PASS (85.1% coverage) |
-| Pre-commit | ✅ PASS |
-| Frontend Tests | ⚠️ 5 failures (unrelated) |
---
## Recommendations
-1. **High Priority**: Fix the 5 failing CrowdSecConfig tests by updating element selectors
-2. **Medium Priority**: Add ARIA attributes to ImportSuccessModal for better accessibility
-3. **Low Priority**: Address ESLint warnings in CrowdSecConfig.tsx (missing deps, any types)
+1. **No blocking issues** - All changes are complete and verified.
+
+2. **Pre-existing warnings** - Consider addressing the `@typescript-eslint/no-explicit-any` warnings in `CrowdSecConfig.tsx` and its test file in a future cleanup pass.
---
## Conclusion
-**PARTIAL PASS** - The audited features (ImportSuccessModal, CertificateStatusCard, useImport) are well-implemented and pass all their tests. The failing tests are in an unrelated component (CrowdSecConfig) and should be addressed in a separate PR.
+The WAF to Coraza rename has been successfully implemented:
-The code demonstrates:
-- Strong TypeScript usage
-- Comprehensive test coverage for the audited features
-- Consistent styling patterns
-- Proper React Query patterns
-- No memory leaks or cleanup issues
+- ✅ UI displays "Coraza" in the Security dashboard card
+- ✅ Navigation shows "Coraza" instead of "WAF"
+- ✅ Dropdowns removed from main Security page (moved to dedicated config page)
+- ✅ All 728 frontend tests pass
+- ✅ TypeScript compiles without errors
+- ✅ No new lint errors introduced
+- ✅ All pre-commit hooks pass
+
+**QA Approval:** ✅ Approved for merge
diff --git a/frontend/package.json b/frontend/package.json
index 517138b4..1f05f31c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,6 +14,7 @@
"type-check": "tsc --noEmit",
"lint": "eslint . --report-unused-disable-directives",
"preview": "vite preview",
+ "test": "vitest run",
"test:ci": "vitest run",
"test:ui": "vitest --ui",
"check-coverage": "bash ../scripts/frontend-test-coverage.sh",
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index 87cd41f0..6581e5ea 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -67,7 +67,7 @@ export default function Layout({ children }: LayoutProps) {
{ name: 'CrowdSec', path: '/security/crowdsec', icon: '🛡️' },
{ name: 'Access Lists', path: '/security/access-lists', icon: '🔒' },
{ name: 'Rate Limiting', path: '/security/rate-limiting', icon: '⚡' },
- { name: 'WAF (Coraza)', path: '/security/waf', icon: '🛡️' },
+ { name: 'Coraza', path: '/security/waf', icon: '🛡️' },
]},
{ name: 'Notifications', path: '/notifications', icon: '🔔' },
// Import group moved under Tasks
diff --git a/frontend/src/pages/Security.tsx b/frontend/src/pages/Security.tsx
index 15291a46..c9ed34db 100644
--- a/frontend/src/pages/Security.tsx
+++ b/frontend/src/pages/Security.tsx
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'
import { useNavigate, Outlet } from 'react-router-dom'
import { Shield, ShieldAlert, ShieldCheck, Lock, Activity, ExternalLink } from 'lucide-react'
import { getSecurityStatus, type SecurityStatus } from '../api/security'
-import { useSecurityConfig, useUpdateSecurityConfig, useGenerateBreakGlassToken, useRuleSets } from '../hooks/useSecurity'
+import { useSecurityConfig, useUpdateSecurityConfig, useGenerateBreakGlassToken } from '../hooks/useSecurity'
import { startCrowdsec, stopCrowdsec, statusCrowdsec } from '../api/crowdsec'
import { updateSetting } from '../api/settings'
import { Switch } from '../components/ui/Switch'
@@ -21,7 +21,6 @@ export default function Security() {
queryFn: getSecurityStatus,
})
const { data: securityConfig } = useSecurityConfig()
- const { data: ruleSetsData } = useRuleSets()
const [adminWhitelist, setAdminWhitelist] = useState('')
const [showNotificationSettings, setShowNotificationSettings] = useState(false)
useEffect(() => {
@@ -168,7 +167,7 @@ export default function Security() {
Cerberus Disabled
- Cerberus powers CrowdSec, WAF, ACLs, and Rate Limiting. Enable the Cerberus toggle in System Settings to awaken the guardian, then configure each head below.
+ Cerberus powers CrowdSec, Coraza, ACLs, and Rate Limiting. Enable the Cerberus toggle in System Settings to awaken the guardian, then configure each head below.