391 lines
11 KiB
Go
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)
|
|
}
|