- Updated Definition of Done report with detailed checks and results for backend and frontend tests. - Documented issues related to race conditions and test failures in QA reports. - Improved security scan notes and code cleanup status in QA reports. - Added summaries for rate limit integration test fixes, including root causes and resolutions. - Introduced new debug and integration scripts for rate limit testing. - Updated security documentation to reflect changes in configuration and troubleshooting steps. - Enhanced troubleshooting guides for CrowdSec and Go language server (gopls) errors. - Improved frontend and scripts README files for clarity and usage instructions.
589 lines
19 KiB
Go
589 lines
19 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/Wikid82/charon/backend/internal/config"
|
|
"github.com/Wikid82/charon/backend/internal/models"
|
|
)
|
|
|
|
// setupAuditTestDB creates an in-memory SQLite database for security audit tests
|
|
func setupAuditTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(
|
|
&models.SecurityConfig{},
|
|
&models.SecurityRuleSet{},
|
|
&models.SecurityDecision{},
|
|
&models.SecurityAudit{},
|
|
&models.Setting{},
|
|
))
|
|
return db
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: SQL Injection Tests
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_GetStatus_SQLInjection(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Seed malicious setting keys that could be used in SQL injection
|
|
maliciousKeys := []string{
|
|
"security.cerberus.enabled'; DROP TABLE settings;--",
|
|
"security.cerberus.enabled\"; DROP TABLE settings;--",
|
|
"security.cerberus.enabled OR 1=1--",
|
|
"security.cerberus.enabled UNION SELECT * FROM users--",
|
|
}
|
|
|
|
for _, key := range maliciousKeys {
|
|
// Attempt to seed with malicious key (should fail or be harmless)
|
|
setting := models.Setting{Key: key, Value: "true"}
|
|
db.Create(&setting)
|
|
}
|
|
|
|
cfg := config.SecurityConfig{CerberusEnabled: false}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should return 200 and valid JSON despite malicious data
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, resp, "cerberus")
|
|
}
|
|
|
|
func TestSecurityHandler_CreateDecision_SQLInjection(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/decisions", h.CreateDecision)
|
|
|
|
// Attempt SQL injection via payload fields
|
|
maliciousPayloads := []map[string]string{
|
|
{"ip": "'; DROP TABLE security_decisions;--", "action": "block"},
|
|
{"ip": "127.0.0.1", "action": "'; DELETE FROM security_decisions;--"},
|
|
{"ip": "\" OR 1=1; --", "action": "allow"},
|
|
{"ip": "127.0.0.1", "action": "block", "details": "'; DROP TABLE users;--"},
|
|
}
|
|
|
|
for i, payload := range maliciousPayloads {
|
|
t.Run(fmt.Sprintf("payload_%d", i), func(t *testing.T) {
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest("POST", "/api/v1/security/decisions", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should return 200 (created) or 400 (bad request) but NOT crash
|
|
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusBadRequest,
|
|
"Expected 200 or 400, got %d", w.Code)
|
|
|
|
// Verify tables still exist
|
|
var count int64
|
|
db.Raw("SELECT COUNT(*) FROM security_decisions").Scan(&count)
|
|
// Should not error from SQL injection
|
|
assert.GreaterOrEqual(t, count, int64(0))
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: Input Validation Tests
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_UpsertRuleSet_MassivePayload(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/rulesets", h.UpsertRuleSet)
|
|
|
|
// Try to submit a 3MB payload (should be rejected by service)
|
|
hugeContent := strings.Repeat("SecRule REQUEST_URI \"@contains /admin\" \"id:1000,phase:1,deny\"\n", 50000)
|
|
|
|
payload := map[string]interface{}{
|
|
"name": "huge-ruleset",
|
|
"content": hugeContent,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/v1/security/rulesets", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should be rejected (either 400 or 500 indicating content too large)
|
|
// The service limits to 2MB
|
|
if len(hugeContent) > 2*1024*1024 {
|
|
assert.True(t, w.Code == http.StatusBadRequest || w.Code == http.StatusInternalServerError,
|
|
"Expected rejection of huge payload, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestSecurityHandler_UpsertRuleSet_EmptyName(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/rulesets", h.UpsertRuleSet)
|
|
|
|
payload := map[string]interface{}{
|
|
"name": "",
|
|
"content": "SecRule REQUEST_URI \"@contains /admin\"",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/v1/security/rulesets", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Contains(t, resp, "error")
|
|
}
|
|
|
|
func TestSecurityHandler_CreateDecision_EmptyFields(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/decisions", h.CreateDecision)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
payload map[string]string
|
|
wantCode int
|
|
}{
|
|
{"empty_ip", map[string]string{"ip": "", "action": "block"}, http.StatusBadRequest},
|
|
{"empty_action", map[string]string{"ip": "127.0.0.1", "action": ""}, http.StatusBadRequest},
|
|
{"both_empty", map[string]string{"ip": "", "action": ""}, http.StatusBadRequest},
|
|
{"valid", map[string]string{"ip": "127.0.0.1", "action": "block"}, http.StatusOK},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
body, _ := json.Marshal(tc.payload)
|
|
req := httptest.NewRequest("POST", "/api/v1/security/decisions", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tc.wantCode, w.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: Settings Toggle Persistence Tests
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_GetStatus_SettingsOverride(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Create SecurityConfig with all security features enabled (DB priority)
|
|
secCfg := &models.SecurityConfig{
|
|
Name: "default", // Required - GetStatus looks for name='default'
|
|
Enabled: true,
|
|
WAFMode: "block", // "block" mode enables WAF
|
|
RateLimitMode: "enabled",
|
|
CrowdSecMode: "local", // "local" mode enables CrowdSec
|
|
RateLimitEnable: true,
|
|
}
|
|
require.NoError(t, db.Create(secCfg).Error)
|
|
|
|
// Seed settings (these won't override DB SecurityConfig for WAF/Rate Limit/CrowdSec)
|
|
settings := []models.Setting{
|
|
{Key: "feature.cerberus.enabled", Value: "true", Category: "feature"},
|
|
{Key: "security.waf.enabled", Value: "true", Category: "security"},
|
|
{Key: "security.rate_limit.enabled", Value: "true", Category: "security"},
|
|
{Key: "security.crowdsec.enabled", Value: "true", Category: "security"},
|
|
}
|
|
for _, s := range settings {
|
|
require.NoError(t, db.Create(&s).Error)
|
|
}
|
|
|
|
// Static config has everything disabled (lowest priority)
|
|
cfg := config.SecurityConfig{
|
|
CerberusEnabled: false,
|
|
WAFMode: "disabled",
|
|
RateLimitMode: "disabled",
|
|
CrowdSecMode: "disabled",
|
|
ACLMode: "enabled", // ACL comes from static config only
|
|
}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
// Verify DB config is used (highest priority) for SecurityConfig features
|
|
assert.True(t, resp["cerberus"]["enabled"].(bool), "cerberus should be enabled via DB config")
|
|
assert.True(t, resp["waf"]["enabled"].(bool), "waf should be enabled via DB config")
|
|
assert.True(t, resp["rate_limit"]["enabled"].(bool), "rate_limit should be enabled via DB config")
|
|
assert.True(t, resp["crowdsec"]["enabled"].(bool), "crowdsec should be enabled via DB config")
|
|
// ACL comes from static config only (not in SecurityConfig model)
|
|
assert.True(t, resp["acl"]["enabled"].(bool), "acl should be enabled via static config")
|
|
}
|
|
|
|
func TestSecurityHandler_GetStatus_DisabledViaSettings(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Seed settings that disable everything
|
|
settings := []models.Setting{
|
|
{Key: "feature.cerberus.enabled", Value: "false", Category: "feature"},
|
|
{Key: "security.waf.enabled", Value: "false", Category: "security"},
|
|
{Key: "security.rate_limit.enabled", Value: "false", Category: "security"},
|
|
{Key: "security.crowdsec.enabled", Value: "false", Category: "security"},
|
|
}
|
|
for _, s := range settings {
|
|
require.NoError(t, db.Create(&s).Error)
|
|
}
|
|
|
|
// Config has everything enabled
|
|
cfg := config.SecurityConfig{
|
|
CerberusEnabled: true,
|
|
WAFMode: "enabled",
|
|
RateLimitMode: "enabled",
|
|
CrowdSecMode: "local",
|
|
}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
// Verify settings override config to disabled
|
|
assert.False(t, resp["cerberus"]["enabled"].(bool), "cerberus should be disabled via settings")
|
|
assert.False(t, resp["waf"]["enabled"].(bool), "waf should be disabled via settings")
|
|
assert.False(t, resp["rate_limit"]["enabled"].(bool), "rate_limit should be disabled via settings")
|
|
assert.False(t, resp["crowdsec"]["enabled"].(bool), "crowdsec should be disabled via settings")
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: Delete RuleSet Validation
|
|
// =============================================================================
|
|
|
|
func TestSecurityAudit_DeleteRuleSet_InvalidID(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.DELETE("/api/v1/security/rulesets/:id", h.DeleteRuleSet)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
id string
|
|
wantCode int
|
|
}{
|
|
{"empty_id", "", http.StatusNotFound}, // gin routes to 404 for missing param
|
|
{"non_numeric", "abc", http.StatusBadRequest},
|
|
{"negative", "-1", http.StatusBadRequest},
|
|
{"sql_injection", "1%3B+DROP+TABLE+security_rule_sets", http.StatusBadRequest},
|
|
{"not_found", "999999", http.StatusNotFound},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
url := "/api/v1/security/rulesets/" + tc.id
|
|
if tc.id == "" {
|
|
url = "/api/v1/security/rulesets/"
|
|
}
|
|
req := httptest.NewRequest("DELETE", url, http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tc.wantCode, w.Code, "ID: %s", tc.id)
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: XSS Prevention (stored XSS in ruleset content)
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_UpsertRuleSet_XSSInContent(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/rulesets", h.UpsertRuleSet)
|
|
router.GET("/api/v1/security/rulesets", h.ListRuleSets)
|
|
|
|
// Store content with XSS payload
|
|
xssPayload := `<script>alert('XSS')</script>`
|
|
payload := map[string]interface{}{
|
|
"name": "xss-test",
|
|
"content": xssPayload,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/v1/security/rulesets", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Accept that content is stored (backend stores as-is, frontend must sanitize)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify it's stored and returned as JSON (not rendered as HTML)
|
|
req2 := httptest.NewRequest("GET", "/api/v1/security/rulesets", http.NoBody)
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
// Content-Type should be application/json
|
|
contentType := w2.Header().Get("Content-Type")
|
|
assert.Contains(t, contentType, "application/json")
|
|
|
|
// The XSS payload should be JSON-escaped, not executable
|
|
assert.Contains(t, w2.Body.String(), `\u003cscript\u003e`)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: Rate Limiting Config Bounds
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_UpdateConfig_RateLimitBounds(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.PUT("/api/v1/security/config", h.UpdateConfig)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
payload map[string]interface{}
|
|
wantOK bool
|
|
}{
|
|
{
|
|
"valid_limits",
|
|
map[string]interface{}{"rate_limit_requests": 100, "rate_limit_burst": 10, "rate_limit_window_sec": 60},
|
|
true,
|
|
},
|
|
{
|
|
"zero_requests",
|
|
map[string]interface{}{"rate_limit_requests": 0, "rate_limit_burst": 10},
|
|
true, // Backend accepts, frontend validates
|
|
},
|
|
{
|
|
"negative_burst",
|
|
map[string]interface{}{"rate_limit_requests": 100, "rate_limit_burst": -1},
|
|
true, // Backend accepts, frontend validates
|
|
},
|
|
{
|
|
"huge_values",
|
|
map[string]interface{}{"rate_limit_requests": 999999999, "rate_limit_burst": 999999999},
|
|
true, // Backend accepts (no upper bound validation currently)
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
body, _ := json.Marshal(tc.payload)
|
|
req := httptest.NewRequest("PUT", "/api/v1/security/config", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if tc.wantOK {
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
} else {
|
|
assert.NotEqual(t, http.StatusOK, w.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: DB Nil Handling
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_GetStatus_NilDB(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// Handler with nil DB should not panic
|
|
cfg := config.SecurityConfig{CerberusEnabled: true}
|
|
h := NewSecurityHandler(cfg, nil, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Should not panic
|
|
assert.NotPanics(t, func() {
|
|
router.ServeHTTP(w, req)
|
|
})
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: Break-Glass Token Security
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_Enable_WithoutWhitelist(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Create config without whitelist
|
|
existingCfg := models.SecurityConfig{Name: "default", AdminWhitelist: ""}
|
|
require.NoError(t, db.Create(&existingCfg).Error)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/enable", h.Enable)
|
|
|
|
// Try to enable without token or whitelist
|
|
req := httptest.NewRequest("POST", "/api/v1/security/enable", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should be rejected
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]string
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Contains(t, resp["error"], "whitelist")
|
|
}
|
|
|
|
func TestSecurityHandler_Disable_RequiresToken(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Create config with break-glass hash
|
|
existingCfg := models.SecurityConfig{Name: "default", Enabled: true}
|
|
require.NoError(t, db.Create(&existingCfg).Error)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/disable", h.Disable)
|
|
|
|
// Try to disable from non-localhost without token
|
|
req := httptest.NewRequest("POST", "/api/v1/security/disable", http.NoBody)
|
|
req.RemoteAddr = "10.0.0.5:12345"
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should be rejected
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY AUDIT: CrowdSec Mode Validation
|
|
// =============================================================================
|
|
|
|
func TestSecurityHandler_GetStatus_CrowdSecModeValidation(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db := setupAuditTestDB(t)
|
|
|
|
// Try to set invalid CrowdSec modes via settings
|
|
invalidModes := []string{"remote", "external", "cloud", "api", "../../../etc/passwd"}
|
|
|
|
for _, mode := range invalidModes {
|
|
t.Run("mode_"+mode, func(t *testing.T) {
|
|
// Clear settings
|
|
db.Exec("DELETE FROM settings")
|
|
|
|
// Set invalid mode
|
|
setting := models.Setting{Key: "security.crowdsec.mode", Value: mode, Category: "security"}
|
|
db.Create(&setting)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
|
|
// Invalid modes should be normalized to "disabled"
|
|
assert.Equal(t, "disabled", resp["crowdsec"]["mode"],
|
|
"Invalid mode '%s' should be normalized to 'disabled'", mode)
|
|
})
|
|
}
|
|
}
|