464 lines
12 KiB
Go
464 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"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"
|
|
)
|
|
|
|
// setupBenchmarkDB creates an in-memory SQLite database for benchmarks
|
|
func setupBenchmarkDB(b *testing.B) *gorm.DB {
|
|
b.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(
|
|
&models.SecurityConfig{},
|
|
&models.SecurityRuleSet{},
|
|
&models.SecurityDecision{},
|
|
&models.SecurityAudit{},
|
|
&models.Setting{},
|
|
&models.ProxyHost{},
|
|
&models.AccessList{},
|
|
&models.User{},
|
|
); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECURITY HANDLER BENCHMARKS
|
|
// =============================================================================
|
|
|
|
func BenchmarkSecurityHandler_GetStatus(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
// Seed settings
|
|
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"},
|
|
{Key: "security.acl.enabled", Value: "true", Category: "security"},
|
|
}
|
|
for _, s := range settings {
|
|
db.Create(&s)
|
|
}
|
|
|
|
cfg := config.SecurityConfig{CerberusEnabled: true}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_GetStatus_NoSettings(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
cfg := config.SecurityConfig{CerberusEnabled: true}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_ListDecisions(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
// Seed some decisions
|
|
for i := 0; i < 100; i++ {
|
|
db.Create(&models.SecurityDecision{
|
|
UUID: "test-uuid-" + string(rune(i)),
|
|
Source: "test",
|
|
Action: "block",
|
|
IP: "192.168.1.1",
|
|
})
|
|
}
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/decisions", h.ListDecisions)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/decisions?limit=50", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_ListRuleSets(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
// Seed some rulesets
|
|
for i := 0; i < 10; i++ {
|
|
db.Create(&models.SecurityRuleSet{
|
|
UUID: "ruleset-uuid-" + string(rune(i)),
|
|
Name: "Ruleset " + string(rune('A'+i)),
|
|
Content: "SecRule REQUEST_URI \"@contains /admin\" \"id:1000,phase:1,deny\"",
|
|
Mode: "blocking",
|
|
})
|
|
}
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/rulesets", h.ListRuleSets)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/rulesets", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_UpsertRuleSet(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/rulesets", h.UpsertRuleSet)
|
|
|
|
payload := map[string]any{
|
|
"name": "bench-ruleset",
|
|
"content": "SecRule REQUEST_URI \"@contains /admin\" \"id:1000,phase:1,deny\"",
|
|
"mode": "blocking",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
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)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_CreateDecision(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/decisions", h.CreateDecision)
|
|
|
|
payload := map[string]any{
|
|
"ip": "192.168.1.100",
|
|
"action": "block",
|
|
"details": "benchmark test",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
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)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_GetConfig(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
// Seed a config
|
|
db.Create(&models.SecurityConfig{
|
|
Name: "default",
|
|
Enabled: true,
|
|
AdminWhitelist: "192.168.1.0/24",
|
|
WAFMode: "block",
|
|
RateLimitEnable: true,
|
|
RateLimitBurst: 10,
|
|
})
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/config", h.GetConfig)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/config", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_UpdateConfig(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.PUT("/api/v1/security/config", h.UpdateConfig)
|
|
|
|
payload := map[string]any{
|
|
"name": "default",
|
|
"enabled": true,
|
|
"rate_limit_enable": true,
|
|
"rate_limit_burst": 10,
|
|
"rate_limit_requests": 100,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
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 w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// PARALLEL BENCHMARKS (Concurrency Testing)
|
|
// =============================================================================
|
|
|
|
func BenchmarkSecurityHandler_GetStatus_Parallel(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
settings := []models.Setting{
|
|
{Key: "feature.cerberus.enabled", Value: "true", Category: "feature"},
|
|
{Key: "security.waf.enabled", Value: "true", Category: "security"},
|
|
}
|
|
for _, s := range settings {
|
|
db.Create(&s)
|
|
}
|
|
|
|
cfg := config.SecurityConfig{CerberusEnabled: true}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
for pb.Next() {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_ListDecisions_Parallel(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
// Use file-based SQLite with WAL mode for parallel testing
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&_journal_mode=WAL"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&models.SecurityDecision{}, &models.SecurityAudit{}); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
db.Create(&models.SecurityDecision{
|
|
UUID: "test-uuid-" + string(rune(i)),
|
|
Source: "test",
|
|
Action: "block",
|
|
IP: "192.168.1.1",
|
|
})
|
|
}
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/decisions", h.ListDecisions)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
for pb.Next() {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/decisions?limit=50", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// MEMORY PRESSURE BENCHMARKS
|
|
// =============================================================================
|
|
|
|
func BenchmarkSecurityHandler_LargeRuleSetContent(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.POST("/api/v1/security/rulesets", h.UpsertRuleSet)
|
|
|
|
// 100KB ruleset content (under 2MB limit)
|
|
largeContent := ""
|
|
for i := 0; i < 1000; i++ {
|
|
largeContent += "SecRule REQUEST_URI \"@contains /path" + string(rune(i)) + "\" \"id:" + string(rune(1000+i)) + ",phase:1,deny\"\n"
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"name": "large-ruleset",
|
|
"content": largeContent,
|
|
"mode": "blocking",
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
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)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkSecurityHandler_ManySettingsLookups(b *testing.B) {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
db := setupBenchmarkDB(b)
|
|
|
|
// Seed many settings
|
|
for i := 0; i < 100; i++ {
|
|
db.Create(&models.Setting{
|
|
Key: "setting.key." + string(rune(i)),
|
|
Value: "value",
|
|
Category: "misc",
|
|
})
|
|
}
|
|
// Security settings
|
|
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"},
|
|
{Key: "security.crowdsec.mode", Value: "local", Category: "security"},
|
|
{Key: "security.crowdsec.api_url", Value: "http://localhost:8080", Category: "security"},
|
|
{Key: "security.acl.enabled", Value: "true", Category: "security"},
|
|
}
|
|
for _, s := range settings {
|
|
db.Create(&s)
|
|
}
|
|
|
|
cfg := config.SecurityConfig{}
|
|
h := NewSecurityHandler(cfg, db, nil)
|
|
|
|
router := gin.New()
|
|
router.GET("/api/v1/security/status", h.GetStatus)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
req := httptest.NewRequest("GET", "/api/v1/security/status", http.NoBody)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
b.Fatalf("unexpected status: %d", w.Code)
|
|
}
|
|
}
|
|
}
|