- 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.
183 lines
5.6 KiB
Go
183 lines
5.6 KiB
Go
package cerberus_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/cerberus"
|
|
"github.com/Wikid82/charon/backend/internal/config"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupDB(t *testing.T) *gorm.DB {
|
|
dsn := fmt.Sprintf("file:cerberus_middleware_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{}, &models.AccessListRule{}))
|
|
return db
|
|
}
|
|
|
|
// TestMiddleware_WAFEnabledTracksMetrics tests that the cerberus middleware tracks WAF metrics
|
|
// when WAF mode is enabled. Note: Actual WAF blocking is handled by Coraza at the Caddy layer,
|
|
// not by this middleware. The middleware only provides metrics tracking and ACL enforcement.
|
|
func TestMiddleware_WAFEnabledTracksMetrics(t *testing.T) {
|
|
db := setupDB(t)
|
|
cfg := config.SecurityConfig{WAFMode: "block"}
|
|
c := cerberus.New(cfg, db)
|
|
|
|
// Setup gin context
|
|
w := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(w)
|
|
|
|
// Create a request - this middleware no longer blocks on payload content
|
|
// because Coraza handles WAF at the Caddy layer
|
|
req := httptest.NewRequest(http.MethodGet, "/?q=test", http.NoBody)
|
|
req.RequestURI = "/?q=test"
|
|
ctx.Request = req
|
|
|
|
// call middleware
|
|
mw := c.Middleware()
|
|
mw(ctx)
|
|
|
|
// Middleware should pass through - it only tracks metrics now
|
|
// WAF blocking happens at Caddy/Coraza layer
|
|
require.False(t, ctx.IsAborted(), "cerberus middleware should not block - WAF is handled by Coraza at Caddy layer")
|
|
}
|
|
|
|
func TestMiddleware_ACLBlocksClientIP(t *testing.T) {
|
|
db := setupDB(t)
|
|
cfg := config.SecurityConfig{ACLMode: "enabled"}
|
|
// Create an ACL that blocks 8.8.8.8
|
|
ruleJSON := `[ { "cidr": "8.8.8.8/32", "description": "block" } ]`
|
|
acl := &models.AccessList{Name: "Block8", Type: "blacklist", IPRules: ruleJSON, Enabled: true}
|
|
require.NoError(t, db.Create(acl).Error)
|
|
|
|
c := cerberus.New(cfg, db)
|
|
|
|
// Setup gin context with remote address 8.8.8.8
|
|
w := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(w)
|
|
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
|
|
req.RemoteAddr = "8.8.8.8:1234"
|
|
ctx.Request = req
|
|
|
|
mw := c.Middleware()
|
|
mw(ctx)
|
|
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
func TestMiddleware_ACLAllowsClientIP(t *testing.T) {
|
|
db := setupDB(t)
|
|
cfg := config.SecurityConfig{ACLMode: "enabled"}
|
|
// Create a whitelist that allows 8.8.8.8
|
|
ruleJSON := `[ { "cidr": "8.8.8.8/32", "description": "allow" } ]`
|
|
acl := &models.AccessList{Name: "Allow8", Type: "whitelist", IPRules: ruleJSON, Enabled: true}
|
|
require.NoError(t, db.Create(acl).Error)
|
|
|
|
c := cerberus.New(cfg, db)
|
|
|
|
// Setup gin context with remote address 8.8.8.8 (allowed)
|
|
w := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(w)
|
|
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
|
|
req.RemoteAddr = "8.8.8.8:1234"
|
|
ctx.Request = req
|
|
|
|
mw := c.Middleware()
|
|
mw(ctx)
|
|
// Should not block - middleware did not abort
|
|
require.False(t, ctx.IsAborted())
|
|
}
|
|
|
|
func TestMiddleware_NotEnabledSkips(t *testing.T) {
|
|
db := setupDB(t)
|
|
// All modes disabled by default
|
|
cfg := config.SecurityConfig{}
|
|
c := cerberus.New(cfg, db)
|
|
|
|
w := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(w)
|
|
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
|
|
req.RemoteAddr = "1.2.3.4:1234"
|
|
ctx.Request = req
|
|
|
|
mw := c.Middleware()
|
|
mw(ctx)
|
|
require.False(t, ctx.IsAborted())
|
|
}
|
|
|
|
func TestMiddleware_WAFPassesWithNoPayload(t *testing.T) {
|
|
db := setupDB(t)
|
|
cfg := config.SecurityConfig{WAFMode: "block"}
|
|
c := cerberus.New(cfg, db)
|
|
|
|
w := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(w)
|
|
req := httptest.NewRequest(http.MethodGet, "/?q=safe", http.NoBody)
|
|
req.RequestURI = "/?q=safe"
|
|
ctx.Request = req
|
|
|
|
mw := c.Middleware()
|
|
mw(ctx)
|
|
require.False(t, ctx.IsAborted())
|
|
}
|
|
|
|
func TestMiddleware_WAFMonitorLogsButDoesNotBlock(t *testing.T) {
|
|
db := setupDB(t)
|
|
cfg := config.SecurityConfig{WAFMode: "monitor"}
|
|
c := cerberus.New(cfg, db)
|
|
|
|
// Test 1: suspicious payload in monitor mode should NOT block
|
|
w := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(w)
|
|
req := httptest.NewRequest(http.MethodGet, "/?q=<script>", http.NoBody)
|
|
req.RequestURI = "/?q=<script>"
|
|
ctx.Request = req
|
|
|
|
mw := c.Middleware()
|
|
mw(ctx)
|
|
require.False(t, ctx.IsAborted(), "monitor mode should not block suspicious payload")
|
|
|
|
// Test 2: safe query in monitor mode should also pass
|
|
w2 := httptest.NewRecorder()
|
|
ctx2, _ := gin.CreateTestContext(w2)
|
|
req2 := httptest.NewRequest(http.MethodGet, "/?q=safe", http.NoBody)
|
|
req2.RequestURI = "/?q=safe"
|
|
ctx2.Request = req2
|
|
|
|
mw2 := c.Middleware()
|
|
mw2(ctx2)
|
|
require.False(t, ctx2.IsAborted(), "monitor mode should not block safe payload")
|
|
}
|
|
|
|
func TestMiddleware_ACLDisabledDoesNotBlock(t *testing.T) {
|
|
db := setupDB(t)
|
|
cfg := config.SecurityConfig{ACLMode: "enabled"}
|
|
// Create a disabled ACL that would block 8.8.8.8 (but it's disabled)
|
|
ruleJSON := `[ { "cidr": "8.8.8.8/32", "description": "block" } ]`
|
|
acl := &models.AccessList{Name: "Block8_Disabled", Type: "blacklist", IPRules: ruleJSON, Enabled: false}
|
|
require.NoError(t, db.Create(acl).Error)
|
|
|
|
c := cerberus.New(cfg, db)
|
|
|
|
// Setup gin context with remote address 8.8.8.8
|
|
w := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(w)
|
|
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
|
|
req.RemoteAddr = "8.8.8.8:1234"
|
|
ctx.Request = req
|
|
|
|
mw := c.Middleware()
|
|
mw(ctx)
|
|
// Disabled ACL should not block
|
|
require.False(t, ctx.IsAborted())
|
|
}
|