package handlers import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/Wikid82/charon/backend/internal/config" "github.com/Wikid82/charon/backend/internal/models" "gorm.io/gorm" ) // Tests for UpdateConfig handler to improve coverage (currently 46%) func TestSecurityHandler_UpdateConfig_Success(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/config", handler.UpdateConfig) payload := map[string]any{ "name": "default", "admin_whitelist": "192.168.1.0/24", "waf_mode": "monitor", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/config", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotNil(t, resp["config"]) } func TestSecurityHandler_UpdateConfig_DefaultName(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityRuleSet{}, &models.SecurityDecision{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/config", handler.UpdateConfig) // Payload without name - should default to "default" payload := map[string]any{ "admin_whitelist": "10.0.0.0/8", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/config", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_UpdateConfig_InvalidPayload(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/config", handler.UpdateConfig) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/config", strings.NewReader("invalid json")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // Tests for GetConfig handler func TestSecurityHandler_GetConfig_Success(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create a config cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "127.0.0.1"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/config", handler.GetConfig) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/config", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotNil(t, resp["config"]) } func TestSecurityHandler_GetConfig_NotFound(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/config", handler.GetConfig) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/config", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Nil(t, resp["config"]) } // Tests for ListDecisions handler func TestSecurityHandler_ListDecisions_Success(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) // Create some decisions with UUIDs db.Create(&models.SecurityDecision{UUID: uuid.New().String(), IP: "1.2.3.4", Action: "block", Source: "waf"}) db.Create(&models.SecurityDecision{UUID: uuid.New().String(), IP: "5.6.7.8", Action: "allow", Source: "acl"}) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/decisions", handler.ListDecisions) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/decisions", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) decisions := resp["decisions"].([]any) assert.Len(t, decisions, 2) } func TestSecurityHandler_ListDecisions_WithLimit(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) // Create 5 decisions with unique UUIDs for i := 0; i < 5; i++ { db.Create(&models.SecurityDecision{UUID: uuid.New().String(), IP: fmt.Sprintf("1.2.3.%d", i), Action: "block", Source: "waf"}) } handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/decisions", handler.ListDecisions) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/decisions?limit=2", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) decisions := resp["decisions"].([]any) assert.Len(t, decisions, 2) } // Tests for CreateDecision handler func TestSecurityHandler_CreateDecision_Success(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/decisions", handler.CreateDecision) payload := map[string]any{ "ip": "10.0.0.1", "action": "block", "reason": "manual block", "details": "Test manual override", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_CreateDecision_MissingIP(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/decisions", handler.CreateDecision) payload := map[string]any{ "action": "block", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_CreateDecision_MissingAction(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/decisions", handler.CreateDecision) payload := map[string]any{ "ip": "10.0.0.1", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_CreateDecision_InvalidPayload(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/decisions", handler.CreateDecision) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", strings.NewReader("invalid")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // Tests for ListRuleSets handler func TestSecurityHandler_ListRuleSets_Success(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) // Create some rulesets with UUIDs db.Create(&models.SecurityRuleSet{UUID: uuid.New().String(), Name: "owasp-crs", Mode: "blocking", Content: "# OWASP rules"}) db.Create(&models.SecurityRuleSet{UUID: uuid.New().String(), Name: "custom", Mode: "detection", Content: "# Custom rules"}) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/rulesets", handler.ListRuleSets) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/rulesets", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) rulesets := resp["rulesets"].([]any) assert.Len(t, rulesets, 2) } // Tests for UpsertRuleSet handler func TestSecurityHandler_UpsertRuleSet_Success(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/rulesets", handler.UpsertRuleSet) payload := map[string]any{ "name": "test-ruleset", "mode": "blocking", "content": "# Test rules", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/rulesets", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_UpsertRuleSet_MissingName(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/rulesets", handler.UpsertRuleSet) payload := map[string]any{ "mode": "blocking", "content": "# Test rules", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/rulesets", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_UpsertRuleSet_InvalidPayload(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/rulesets", handler.UpsertRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/rulesets", strings.NewReader("invalid")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // Tests for DeleteRuleSet handler (currently 52%) func TestSecurityHandler_DeleteRuleSet_Success(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{}, &models.SecurityAudit{})) // Create a ruleset to delete ruleset := models.SecurityRuleSet{Name: "delete-me", Mode: "blocking"} db.Create(&ruleset) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/1", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.True(t, resp["deleted"].(bool)) } func TestSecurityHandler_DeleteRuleSet_NotFound(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/999", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestSecurityHandler_DeleteRuleSet_InvalidID(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/invalid", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSecurityHandler_DeleteRuleSet_EmptyID(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityRuleSet{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) // Note: This route pattern won't match empty ID, but testing the handler directly router.DELETE("/security/rulesets/:id", handler.DeleteRuleSet) // This should hit the "id is required" check if we bypass routing w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/security/rulesets/", http.NoBody) router.ServeHTTP(w, req) // Router won't match this path, so 404 assert.Equal(t, http.StatusNotFound, w.Code) } // Tests for Enable handler func TestSecurityHandler_Enable_NoConfigNoWhitelist(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) // Should succeed when no config exists - creates new config assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Enable_WithWhitelist(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with whitelist containing 127.0.0.1 cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "127.0.0.1"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "127.0.0.1:12345" // Use RemoteAddr for ClientIP router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Enable_IPNotInWhitelist(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with whitelist that doesn't include test IP cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "10.0.0.0/8"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "192.168.1.1:12345" // Not in 10.0.0.0/8 router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestSecurityHandler_Enable_WithValidBreakGlassToken(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/breakglass/generate", handler.GenerateBreakGlass) router.POST("/security/enable", handler.Enable) // First, create a config with no whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: ""} db.Create(&cfg) // Generate a break-glass token w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/breakglass/generate", http.NoBody) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var tokenResp map[string]string err := json.Unmarshal(w.Body.Bytes(), &tokenResp) require.NoError(t, err, "Failed to unmarshal response") token := tokenResp["token"] // Now try to enable with the token payload := map[string]string{"break_glass_token": token} body, _ := json.Marshal(payload) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/security/enable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Enable_WithInvalidBreakGlassToken(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with no whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: ""} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) payload := map[string]string{"break_glass_token": "invalid-token"} body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // Tests for Disable handler (currently 44%) func TestSecurityHandler_Disable_FromLocalhost(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { // Simulate localhost request c.Request.RemoteAddr = "127.0.0.1:12345" handler.Disable(c) }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err, "Failed to unmarshal response") assert.False(t, resp["enabled"].(bool)) } func TestSecurityHandler_Disable_FromRemoteWithToken(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/breakglass/generate", handler.GenerateBreakGlass) router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "192.168.1.100:12345" // Remote IP handler.Disable(c) }) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) // Generate token w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/breakglass/generate", http.NoBody) router.ServeHTTP(w, req) var tokenResp map[string]string _ = json.Unmarshal(w.Body.Bytes(), &tokenResp) token := tokenResp["token"] // Disable with token payload := map[string]string{"break_glass_token": token} body, _ := json.Marshal(payload) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/security/disable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_Disable_FromRemoteNoToken(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "192.168.1.100:12345" // Remote IP handler.Disable(c) }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestSecurityHandler_Disable_FromRemoteInvalidToken(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "192.168.1.100:12345" // Remote IP handler.Disable(c) }) payload := map[string]string{"break_glass_token": "invalid-token"} body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // Tests for GenerateBreakGlass handler func TestSecurityHandler_GenerateBreakGlass_NoConfig(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/breakglass/generate", handler.GenerateBreakGlass) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/breakglass/generate", http.NoBody) router.ServeHTTP(w, req) // Should succeed and create a new config with the token assert.Equal(t, http.StatusOK, w.Code) var resp map[string]any err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.NotEmpty(t, resp["token"]) } // Test Enable with IPv6 localhost func TestSecurityHandler_Disable_FromIPv6Localhost(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create enabled config cfg := models.SecurityConfig{Name: "default", Enabled: true} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/disable", func(c *gin.Context) { c.Request.RemoteAddr = "[::1]:12345" // IPv6 localhost handler.Disable(c) }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/disable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // Test Enable with CIDR whitelist matching func TestSecurityHandler_Enable_WithCIDRWhitelist(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with CIDR whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "192.168.0.0/16, 10.0.0.0/8"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "192.168.1.50:12345" // In 192.168.0.0/16 router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // Test Enable with exact IP in whitelist func TestSecurityHandler_Enable_WithExactIPWhitelist(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) // Create config with exact IP whitelist cfg := models.SecurityConfig{Name: "default", AdminWhitelist: "192.168.1.100"} db.Create(&cfg) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.POST("/security/enable", handler.Enable) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/enable", strings.NewReader("{}")) req.Header.Set("Content-Type", "application/json") req.RemoteAddr = "192.168.1.100:12345" router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_GetStatus_BackwardCompatibilityOverrides(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.Setting{}, &models.CaddyConfig{})) require.NoError(t, db.Create(&models.SecurityConfig{ Name: "default", Enabled: true, WAFMode: "block", RateLimitMode: "enabled", CrowdSecMode: "local", }).Error) seed := []models.Setting{ {Key: "security.cerberus.enabled", Value: "false", Category: "security", Type: "bool"}, {Key: "security.crowdsec.mode", Value: "external", Category: "security", Type: "string"}, {Key: "security.waf.enabled", Value: "true", Category: "security", Type: "bool"}, {Key: "security.rate_limit.enabled", Value: "true", Category: "security", Type: "bool"}, {Key: "security.acl.enabled", Value: "true", Category: "security", Type: "bool"}, } for _, setting := range seed { require.NoError(t, db.Create(&setting).Error) } handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.GET("/security/status", handler.GetStatus) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/security/status", http.NoBody) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) var resp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) cerberus := resp["cerberus"].(map[string]any) require.Equal(t, false, cerberus["enabled"]) crowdsec := resp["crowdsec"].(map[string]any) require.Equal(t, "disabled", crowdsec["mode"]) require.Equal(t, false, crowdsec["enabled"]) } func TestSecurityHandler_AddWAFExclusion_InvalidExistingJSONStillAdds(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", WAFExclusions: "{"}).Error) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/waf/exclusions", handler.AddWAFExclusion) body := `{"rule_id":942100,"target":"ARGS:user","description":"test"}` w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/security/waf/exclusions", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) } func TestSecurityHandler_ToggleSecurityModule_SnapshotSettingsError(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) sqlDB, err := db.DB() require.NoError(t, err) require.NoError(t, sqlDB.Close()) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/waf/enable", handler.EnableWAF) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/security/waf/enable", http.NoBody) router.ServeHTTP(w, req) require.Equal(t, http.StatusInternalServerError, w.Code) require.Contains(t, w.Body.String(), "Failed to update security module") } func TestSecurityHandler_ToggleSecurityModule_SnapshotSecurityConfigError(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) require.NoError(t, db.Exec("DROP TABLE security_configs").Error) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/waf/enable", handler.EnableWAF) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/security/waf/enable", http.NoBody) router.ServeHTTP(w, req) require.Equal(t, http.StatusInternalServerError, w.Code) require.Contains(t, w.Body.String(), "Failed to update security module") } func TestSecurityHandler_SnapshotAndRestoreHelpers(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.Setting{}, &models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) require.NoError(t, db.Create(&models.Setting{Key: "k1", Value: "v1", Category: "security", Type: "string"}).Error) snapshots, err := handler.snapshotSettings([]string{"k1", "k1", "k2"}) require.NoError(t, err) require.Len(t, snapshots, 2) require.True(t, snapshots["k1"].exists) require.False(t, snapshots["k2"].exists) require.NoError(t, handler.restoreSettings(map[string]settingSnapshot{ "k1": snapshots["k1"], "k2": snapshots["k2"], })) require.NoError(t, db.Exec("DROP TABLE settings").Error) err = handler.restoreSettings(map[string]settingSnapshot{ "k1": snapshots["k1"], }) require.Error(t, err) } func TestSecurityHandler_DefaultSecurityConfigStateHelpers(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) exists, enabled, err := handler.snapshotDefaultSecurityConfigState() require.NoError(t, err) require.False(t, exists) require.False(t, enabled) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", Enabled: true}).Error) exists, enabled, err = handler.snapshotDefaultSecurityConfigState() require.NoError(t, err) require.True(t, exists) require.True(t, enabled) require.NoError(t, handler.restoreDefaultSecurityConfigState(true, false)) var cfg models.SecurityConfig require.NoError(t, db.Where("name = ?", "default").First(&cfg).Error) require.False(t, cfg.Enabled) require.NoError(t, handler.restoreDefaultSecurityConfigState(false, false)) err = db.Where("name = ?", "default").First(&cfg).Error require.ErrorIs(t, err, gorm.ErrRecordNotFound) } func TestSecurityHandler_EnsureSecurityConfigEnabled_Helper(t *testing.T) { handler := &SecurityHandler{db: nil} err := handler.ensureSecurityConfigEnabled() require.Error(t, err) require.Contains(t, err.Error(), "database not configured") db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityConfig{})) require.NoError(t, db.Create(&models.SecurityConfig{Name: "default", Enabled: false}).Error) handler = NewSecurityHandler(config.SecurityConfig{}, db, nil) require.NoError(t, handler.ensureSecurityConfigEnabled()) var cfg models.SecurityConfig require.NoError(t, db.Where("name = ?", "default").First(&cfg).Error) require.True(t, cfg.Enabled) } func TestLatestConfigApplyState_Helper(t *testing.T) { state := latestConfigApplyState(nil) require.Equal(t, false, state["available"]) db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.CaddyConfig{})) state = latestConfigApplyState(db) require.Equal(t, false, state["available"]) require.NoError(t, db.Create(&models.CaddyConfig{Success: true}).Error) state = latestConfigApplyState(db) require.Equal(t, true, state["available"]) require.Equal(t, "applied", state["status"]) } // TestSecurityHandler_CreateDecision_StripsEnrichmentFields verifies that // clients cannot inject system-populated enrichment fields (Scenario, Country, // ExpiresAt) via the CreateDecision endpoint. func TestSecurityHandler_CreateDecision_StripsEnrichmentFields(t *testing.T) { db := setupTestDB(t) require.NoError(t, db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) router := gin.New() router.Use(func(c *gin.Context) { c.Set("role", "admin") c.Next() }) router.POST("/security/decisions", handler.CreateDecision) payload := map[string]any{ "ip": "10.0.0.1", "action": "block", "details": "test", "scenario": "injected-scenario", "country": "XX", "expires_at": "2099-01-01T00:00:00Z", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/security/decisions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) // Verify the stored decision has empty enrichment fields var stored models.SecurityDecision require.NoError(t, db.First(&stored).Error) assert.Empty(t, stored.Scenario, "Scenario should not be settable by client") assert.Empty(t, stored.Country, "Country should not be settable by client") assert.Nil(t, stored.ExpiresAt, "ExpiresAt should not be settable by client") assert.Equal(t, "manual", stored.Source, "Source must be forced to manual") }