Files
Charon/backend/internal/cerberus/cerberus_test.go
GitHub Actions 25082778c9 feat(cerberus): integrate Cerberus security features (WAF, ACLs, rate limiting, CrowdSec)
- Implement GeoIPService for IP-to-country lookups with comprehensive error handling.
- Add tests for GeoIPService covering various scenarios including invalid IPs and database loading.
- Extend AccessListService to handle GeoIP service integration, including graceful degradation when GeoIP service is unavailable.
- Introduce new tests for AccessListService to validate geo ACL behavior and country code parsing.
- Update SecurityService to include new fields for WAF configuration and enhance decision logging functionality.
- Add extensive tests for SecurityService covering rule set management and decision logging.
- Create a detailed Security Coverage QA Plan to ensure 100% code coverage for security-related functionality.
2025-12-12 17:56:30 +00:00

263 lines
7.2 KiB
Go

package cerberus_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/Wikid82/charon/backend/internal/cerberus"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/stretchr/testify/require"
)
func init() {
gin.SetMode(gin.TestMode)
}
func setupTestDB(t *testing.T) *gorm.DB {
// Use a unique in-memory database per test run to avoid shared state.
dsn := fmt.Sprintf("file:cerberus_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
// migrate only the Setting model used by Cerberus
require.NoError(t, db.AutoMigrate(&models.Setting{}))
return db
}
func setupFullTestDB(t *testing.T) *gorm.DB {
dsn := fmt.Sprintf("file:cerberus_full_test_%d?mode=memory&cache=shared", time.Now().UnixNano())
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(
&models.Setting{},
&models.AccessList{},
))
return db
}
func TestCerberus_IsEnabled_ConfigTrue(t *testing.T) {
db := setupTestDB(t)
cfg := config.SecurityConfig{CerberusEnabled: true}
cerb := cerberus.New(cfg, db)
require.True(t, cerb.IsEnabled())
}
func TestCerberus_IsEnabled_DBSetting(t *testing.T) {
db := setupTestDB(t)
// We're storing 'feature.cerberus.enabled' key
db.Create(&models.Setting{Key: "feature.cerberus.enabled", Value: "true"})
cfg := config.SecurityConfig{CerberusEnabled: false}
cerb := cerberus.New(cfg, db)
require.True(t, cerb.IsEnabled())
}
func TestCerberus_IsEnabled_Disabled(t *testing.T) {
db := setupTestDB(t)
// Per Optional Features spec: when no DB setting exists and no config modes are enabled,
// Cerberus defaults to true (enabled). To test disabled state, we must set DB flag to false.
db.Create(&models.Setting{Key: "feature.cerberus.enabled", Value: "false"})
cfg := config.SecurityConfig{CerberusEnabled: false}
cerb := cerberus.New(cfg, db)
t.Logf("cfg: %+v", cfg)
t.Logf("IsEnabled() -> %v", cerb.IsEnabled())
require.False(t, cerb.IsEnabled())
}
func TestCerberus_IsEnabled_CrowdSecLocal(t *testing.T) {
db := setupTestDB(t)
cfg := config.SecurityConfig{CrowdSecMode: "local"}
cerb := cerberus.New(cfg, db)
require.True(t, cerb.IsEnabled())
}
func TestCerberus_IsEnabled_WAFEnabled(t *testing.T) {
db := setupTestDB(t)
cfg := config.SecurityConfig{WAFMode: "enabled"}
cerb := cerberus.New(cfg, db)
require.True(t, cerb.IsEnabled())
}
func TestCerberus_IsEnabled_RateLimitEnabled(t *testing.T) {
db := setupTestDB(t)
cfg := config.SecurityConfig{RateLimitMode: "enabled"}
cerb := cerberus.New(cfg, db)
require.True(t, cerb.IsEnabled())
}
func TestCerberus_IsEnabled_ACLEnabled(t *testing.T) {
db := setupTestDB(t)
cfg := config.SecurityConfig{ACLMode: "enabled"}
cerb := cerberus.New(cfg, db)
require.True(t, cerb.IsEnabled())
}
func TestCerberus_IsEnabled_LegacySetting(t *testing.T) {
db := setupTestDB(t)
// Test legacy setting key for backward compatibility
db.Create(&models.Setting{Key: "security.cerberus.enabled", Value: "true"})
cfg := config.SecurityConfig{CerberusEnabled: false}
cerb := cerberus.New(cfg, db)
require.True(t, cerb.IsEnabled())
}
// ============================================
// Middleware Tests
// ============================================
func TestCerberus_Middleware_Disabled(t *testing.T) {
db := setupTestDB(t)
// Explicitly disable cerberus via DB setting
db.Create(&models.Setting{Key: "feature.cerberus.enabled", Value: "false"})
cfg := config.SecurityConfig{CerberusEnabled: false}
cerb := cerberus.New(cfg, db)
router := gin.New()
router.Use(cerb.Middleware())
router.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", http.NoBody)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "OK", w.Body.String())
}
func TestCerberus_Middleware_WAFEnabled(t *testing.T) {
db := setupFullTestDB(t)
cfg := config.SecurityConfig{
CerberusEnabled: true,
WAFMode: "enabled",
}
cerb := cerberus.New(cfg, db)
router := gin.New()
router.Use(cerb.Middleware())
router.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", http.NoBody)
router.ServeHTTP(w, req)
// WAF doesn't block in middleware (handled by Caddy), just tracks metrics
require.Equal(t, http.StatusOK, w.Code)
}
func TestCerberus_Middleware_ACLEnabled_NoAccessLists(t *testing.T) {
db := setupFullTestDB(t)
cfg := config.SecurityConfig{
CerberusEnabled: true,
ACLMode: "enabled",
}
cerb := cerberus.New(cfg, db)
router := gin.New()
router.Use(cerb.Middleware())
router.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", http.NoBody)
router.ServeHTTP(w, req)
// No access lists, so request should pass
require.Equal(t, http.StatusOK, w.Code)
}
func TestCerberus_Middleware_ACLEnabled_DisabledList(t *testing.T) {
db := setupFullTestDB(t)
// Create a disabled ACL
db.Create(&models.AccessList{
UUID: "test-acl",
Name: "Test ACL",
Type: "blacklist",
IPRules: `[{"cidr":"0.0.0.0/0","description":"Block all"}]`,
Enabled: false, // Disabled
})
cfg := config.SecurityConfig{
CerberusEnabled: true,
ACLMode: "enabled",
}
cerb := cerberus.New(cfg, db)
router := gin.New()
router.Use(cerb.Middleware())
router.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", http.NoBody)
router.ServeHTTP(w, req)
// ACL is disabled, so request should pass
require.Equal(t, http.StatusOK, w.Code)
}
func TestCerberus_Middleware_ACLEnabled_Blocked(t *testing.T) {
db := setupFullTestDB(t)
// Create an enabled blacklist that blocks 192.168.x.x
db.Create(&models.AccessList{
UUID: "test-blacklist",
Name: "Block Private",
Type: "blacklist",
IPRules: `[{"cidr":"192.168.0.0/16","description":"Block 192.168.x.x"}]`,
Enabled: true,
})
cfg := config.SecurityConfig{
CerberusEnabled: true,
ACLMode: "enabled",
}
cerb := cerberus.New(cfg, db)
router := gin.New()
router.Use(cerb.Middleware())
router.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", http.NoBody)
req.RemoteAddr = "192.168.1.100:12345" // IP in blacklist range
router.ServeHTTP(w, req)
// Request should be blocked
require.Equal(t, http.StatusForbidden, w.Code)
}
func TestCerberus_Middleware_CrowdSecLocal(t *testing.T) {
db := setupFullTestDB(t)
cfg := config.SecurityConfig{
CerberusEnabled: true,
CrowdSecMode: "local",
}
cerb := cerberus.New(cfg, db)
router := gin.New()
router.Use(cerb.Middleware())
router.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "OK")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", http.NoBody)
router.ServeHTTP(w, req)
// CrowdSec doesn't block in middleware (handled by Caddy), just tracks metrics
require.Equal(t, http.StatusOK, w.Code)
}