Files
Charon/backend/internal/cerberus/cerberus_blockers_test.go
2026-03-04 18:34:49 +00:00

391 lines
11 KiB
Go

package cerberus
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// TestBlocker1_SecurityEventProduction tests that all security event types are dispatched to the Cerberus path.
func TestBlocker1_SecurityEventProduction(t *testing.T) {
// Setup test database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// Run migrations
err = db.AutoMigrate(&models.Setting{}, &models.NotificationProvider{}, &models.AccessList{})
assert.NoError(t, err)
// Enable feature flag
setting := models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
Category: "feature",
}
assert.NoError(t, db.Create(&setting).Error)
// Create discord provider with all security events enabled
provider := models.NotificationProvider{
ID: "test-provider",
Name: "Test Discord",
Type: "discord",
URL: "https://discord.com/api/webhooks/123/abc",
Enabled: true,
NotifySecurityWAFBlocks: true,
NotifySecurityACLDenies: true,
NotifySecurityRateLimitHits: true,
NotifySecurityCrowdSecDecisions: true,
}
assert.NoError(t, db.Create(&provider).Error)
// Create Cerberus instance
cfg := config.SecurityConfig{
CerberusEnabled: true,
WAFMode: "enabled",
ACLMode: "enabled",
RateLimitMode: "enabled",
CrowdSecMode: "local",
}
cerberus := New(cfg, db)
// Test each event type
eventTypes := []string{"waf_block", "acl_deny", "rate_limit", "crowdsec_decision"}
for _, eventType := range eventTypes {
t.Run(eventType, func(t *testing.T) {
event := models.SecurityEvent{
EventType: eventType,
Severity: "warn",
Message: "Test " + eventType,
ClientIP: "192.168.1.1",
Path: "/test",
Timestamp: time.Now(),
Metadata: map[string]any{"test": "data"},
}
// Send security event
err := cerberus.sendSecurityNotification(context.Background(), event)
assert.NoError(t, err)
})
}
}
// TestBlocker1_NotifySecurityEventMethod tests the new NotifySecurityEvent method.
func TestBlocker1_NotifySecurityEventMethod(t *testing.T) {
// Setup test database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// Run migrations
err = db.AutoMigrate(&models.Setting{}, &models.NotificationProvider{}, &models.AccessList{})
assert.NoError(t, err)
// Enable feature flag
setting := models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
Category: "feature",
}
assert.NoError(t, db.Create(&setting).Error)
// Create discord provider
provider := models.NotificationProvider{
ID: "test-provider",
Name: "Test Discord",
Type: "discord",
URL: "https://discord.com/api/webhooks/123/abc",
Enabled: true,
NotifySecurityWAFBlocks: true,
}
assert.NoError(t, db.Create(&provider).Error)
// Create Cerberus instance
cfg := config.SecurityConfig{
CerberusEnabled: true,
WAFMode: "enabled",
}
cerberus := New(cfg, db)
// Create test context
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request, _ = http.NewRequest("POST", "/test", nil)
event := models.SecurityEvent{
EventType: "waf_block",
Severity: "warn",
Message: "Test WAF block",
ClientIP: "192.168.1.1",
Path: "/test",
Timestamp: time.Now(),
}
// Test NotifySecurityEvent method
err = cerberus.NotifySecurityEvent(ctx, event)
assert.NoError(t, err)
}
// TestBlocker2_FailClosedWhenFlagAbsent tests that no legacy fallback occurs when flag is absent.
func TestBlocker2_FailClosedWhenFlagAbsent(t *testing.T) {
// Setup test database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// Run migrations
err = db.AutoMigrate(&models.Setting{}, &models.NotificationProvider{}, &models.AccessList{})
assert.NoError(t, err)
// DO NOT create feature flag - it should be absent
// Create Cerberus instance
cfg := config.SecurityConfig{
CerberusEnabled: true,
ACLMode: "enabled",
}
cerberus := New(cfg, db)
event := models.SecurityEvent{
EventType: "acl_deny",
Severity: "warn",
Message: "Test ACL deny",
ClientIP: "192.168.1.1",
Path: "/test",
Timestamp: time.Now(),
}
// Send security event - should fail closed (no error, but no notification sent)
err = cerberus.sendSecurityNotification(context.Background(), event)
assert.NoError(t, err)
}
// TestBlocker2_FailClosedWhenFlagFalse tests that no legacy fallback occurs when flag is false.
func TestBlocker2_FailClosedWhenFlagFalse(t *testing.T) {
// Setup test database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// Run migrations
err = db.AutoMigrate(&models.Setting{}, &models.NotificationProvider{}, &models.AccessList{})
assert.NoError(t, err)
// Create feature flag as FALSE
setting := models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "false",
Type: "bool",
Category: "feature",
}
assert.NoError(t, db.Create(&setting).Error)
// Create Cerberus instance
cfg := config.SecurityConfig{
CerberusEnabled: true,
ACLMode: "enabled",
}
cerberus := New(cfg, db)
event := models.SecurityEvent{
EventType: "acl_deny",
Severity: "warn",
Message: "Test ACL deny",
ClientIP: "192.168.1.1",
Path: "/test",
Timestamp: time.Now(),
}
// Send security event - should fail closed (no error, but no notification sent)
err = cerberus.sendSecurityNotification(context.Background(), event)
assert.NoError(t, err)
}
// TestBlocker2_DispatchWhenFlagTrue tests that provider dispatch occurs when flag is true.
func TestBlocker2_DispatchWhenFlagTrue(t *testing.T) {
// Setup test database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// Run migrations
err = db.AutoMigrate(&models.Setting{}, &models.NotificationProvider{}, &models.AccessList{})
assert.NoError(t, err)
// Create feature flag as TRUE
setting := models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
Category: "feature",
}
assert.NoError(t, db.Create(&setting).Error)
// Create discord provider
provider := models.NotificationProvider{
ID: "test-provider",
Name: "Test Discord",
Type: "discord",
URL: "https://discord.com/api/webhooks/123/abc",
Enabled: true,
NotifySecurityACLDenies: true,
}
assert.NoError(t, db.Create(&provider).Error)
// Create Cerberus instance
cfg := config.SecurityConfig{
CerberusEnabled: true,
ACLMode: "enabled",
}
cerberus := New(cfg, db)
event := models.SecurityEvent{
EventType: "acl_deny",
Severity: "warn",
Message: "Test ACL deny",
ClientIP: "192.168.1.1",
Path: "/test",
Timestamp: time.Now(),
}
// Send security event - should dispatch to provider
err = cerberus.sendSecurityNotification(context.Background(), event)
// Note: Will fail with network error since discord.com/api/webhooks/123/abc is fake
// But that's OK - we're testing the dispatch path, not the actual webhook delivery
// The error should be from the HTTP client, not from missing provider dispatch
// For this test, we just verify no panic and the code path is exercised
_ = err // Ignore network errors for this test
}
// TestNotifySecurityEvent_Disabled covers lines 264-265
func TestNotifySecurityEvent_Disabled(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
err = db.AutoMigrate(&models.Setting{}, &models.NotificationProvider{}, &models.SecurityConfig{})
assert.NoError(t, err)
assert.NoError(t, db.Create(&models.Setting{
Key: "feature.cerberus.enabled",
Value: "false",
Type: "bool",
Category: "feature",
}).Error)
// Create Cerberus instance with disabled security
cfg := config.SecurityConfig{
CerberusEnabled: false,
}
cerberus := New(cfg, db)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request, _ = http.NewRequest("POST", "/test", nil)
event := models.SecurityEvent{
EventType: "waf_block",
Severity: "warn",
Message: "Test",
ClientIP: "192.168.1.1",
Path: "/test",
Timestamp: time.Now(),
}
// Should return nil when disabled (lines 264-265)
err = cerberus.NotifySecurityEvent(ctx, event)
assert.NoError(t, err)
}
// TestSendSecurityNotification_NilDB covers lines 315-316
func TestSendSecurityNotification_NilDB(t *testing.T) {
cfg := config.SecurityConfig{
CerberusEnabled: true,
}
cerberus := New(cfg, nil)
event := models.SecurityEvent{
EventType: "acl_deny",
Severity: "warn",
Message: "Test",
ClientIP: "192.168.1.1",
Path: "/test",
Timestamp: time.Now(),
}
// Should return nil when db is nil (lines 315-316)
err := cerberus.sendSecurityNotification(context.Background(), event)
assert.NoError(t, err)
}
// TestBlocker2_ACLDenyNotificationInMiddleware tests ACL deny notification in actual middleware flow.
func TestBlocker2_ACLDenyNotificationInMiddleware(t *testing.T) {
// Setup test database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
assert.NoError(t, err)
// Run migrations
err = db.AutoMigrate(&models.Setting{}, &models.NotificationProvider{}, &models.AccessList{}, &models.SecurityConfig{})
assert.NoError(t, err)
// Enable feature flag
setting := models.Setting{
Key: "feature.notifications.security_provider_events.enabled",
Value: "true",
Type: "bool",
Category: "feature",
}
assert.NoError(t, db.Create(&setting).Error)
// Create discord provider with ACL denies enabled
provider := models.NotificationProvider{
ID: "test-provider",
Name: "Test Discord",
Type: "discord",
URL: "https://discord.com/api/webhooks/123/abc",
Enabled: true,
NotifySecurityACLDenies: true,
}
assert.NoError(t, db.Create(&provider).Error)
// Create an ACL that denies access
acl := models.AccessList{
UUID: "test-acl",
Name: "Test ACL",
Type: "whitelist",
Enabled: true,
IPRules: `[{"cidr":"10.0.0.0/8","description":"allow private network"}]`,
}
assert.NoError(t, db.Create(&acl).Error)
// Create Cerberus instance
cfg := config.SecurityConfig{
CerberusEnabled: true,
ACLMode: "enabled",
}
cerberus := New(cfg, db)
// Create test context with IP that will be denied
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request, _ = http.NewRequest("GET", "/test", bytes.NewReader([]byte{}))
ctx.Request.RemoteAddr = "192.168.1.1:12345" // IP not in whitelist
// Run middleware
middleware := cerberus.Middleware()
middleware(ctx)
// Should be forbidden
assert.Equal(t, http.StatusForbidden, w.Code)
}