chore: git cache cleanup
This commit is contained in:
@@ -0,0 +1,491 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestSecurityEventIntakeCompileSuccess tests that the handler is properly instantiated and route registration doesn't fail.
|
||||
// Blocker 1: Fix compile error - undefined handler reference.
|
||||
func TestSecurityEventIntakeCompileSuccess(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
// This test validates that the handler can be instantiated with all required dependencies
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
securityService := services.NewSecurityService(db)
|
||||
managementCIDRs := []string{"127.0.0.0/8"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
securityService,
|
||||
"/data",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
require.NotNil(t, handler, "Handler should be instantiated successfully")
|
||||
require.NotNil(t, handler.notificationService, "Notification service should be set")
|
||||
require.NotNil(t, handler.managementCIDRs, "Management CIDRs should be set")
|
||||
assert.Equal(t, 1, len(handler.managementCIDRs), "Management CIDRs should have one entry")
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeAuthLocalhost tests that localhost requests are accepted.
|
||||
// Blocker 2: Implement real source validation - localhost should be allowed.
|
||||
func TestSecurityEventIntakeAuthLocalhost(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"10.0.0.0/8"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
event := models.SecurityEvent{
|
||||
EventType: "waf_block",
|
||||
Severity: "error",
|
||||
Message: "WAF blocked request",
|
||||
ClientIP: "192.168.1.100",
|
||||
Path: "/admin",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
// Localhost IPv4 request
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
||||
c.Request.RemoteAddr = "127.0.0.1:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusAccepted, w.Code, "Localhost should be accepted")
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeAuthManagementCIDR tests that management network requests are accepted.
|
||||
// Blocker 2: Implement real source validation - management CIDRs should be allowed.
|
||||
func TestSecurityEventIntakeAuthManagementCIDR(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"192.168.1.0/24", "10.0.0.0/8"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
event := models.SecurityEvent{
|
||||
EventType: "waf_block",
|
||||
Severity: "error",
|
||||
Message: "WAF blocked request",
|
||||
ClientIP: "8.8.8.8",
|
||||
Path: "/admin",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
// Request from management network
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
||||
c.Request.RemoteAddr = "192.168.1.50:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusAccepted, w.Code, "Management CIDR should be accepted")
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeAuthUnauthorizedIP tests that external IPs are rejected.
|
||||
// Blocker 2: Implement real source validation - external IPs should be rejected.
|
||||
func TestSecurityEventIntakeAuthUnauthorizedIP(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"192.168.1.0/24"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
event := models.SecurityEvent{
|
||||
EventType: "waf_block",
|
||||
Severity: "error",
|
||||
Message: "WAF blocked request",
|
||||
ClientIP: "8.8.8.8",
|
||||
Path: "/admin",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
// External IP not in management network
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
||||
c.Request.RemoteAddr = "8.8.8.8:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code, "External IP should be rejected")
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "unauthorized_source", response["error"])
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeAuthInvalidIP tests that malformed IPs are rejected.
|
||||
// Blocker 2: Implement real source validation - invalid IPs should be rejected.
|
||||
func TestSecurityEventIntakeAuthInvalidIP(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"192.168.1.0/24"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
event := models.SecurityEvent{
|
||||
EventType: "waf_block",
|
||||
Severity: "error",
|
||||
Message: "WAF blocked request",
|
||||
ClientIP: "8.8.8.8",
|
||||
Path: "/admin",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
// Malformed IP
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
||||
c.Request.RemoteAddr = "invalid-ip:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code, "Invalid IP should be rejected")
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "invalid_source", response["error"])
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeDispatchInvoked tests that notification dispatch is invoked.
|
||||
// Blocker 3: Implement actual dispatch - remove TODO/no-op behavior.
|
||||
func TestSecurityEventIntakeDispatchInvoked(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
// Create a Discord provider with security notifications enabled
|
||||
provider := &models.NotificationProvider{
|
||||
Name: "Discord Security Alerts",
|
||||
Type: "discord",
|
||||
URL: "https://discord.com/api/webhooks/123/token",
|
||||
Enabled: true,
|
||||
NotifySecurityWAFBlocks: true,
|
||||
NotifySecurityACLDenies: true,
|
||||
NotifySecurityRateLimitHits: true,
|
||||
NotifySecurityCrowdSecDecisions: true,
|
||||
}
|
||||
require.NoError(t, db.Create(provider).Error)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"127.0.0.0/8"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
event := models.SecurityEvent{
|
||||
EventType: "waf_block",
|
||||
Severity: "error",
|
||||
Message: "WAF blocked suspicious request",
|
||||
ClientIP: "192.168.1.100",
|
||||
Path: "/admin/login",
|
||||
Timestamp: time.Now(),
|
||||
Metadata: map[string]any{
|
||||
"rule_id": "920100",
|
||||
"matched_data": "suspicious pattern",
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
||||
c.Request.RemoteAddr = "127.0.0.1:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusAccepted, w.Code, "Event should be accepted")
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Security event recorded", response["message"])
|
||||
assert.Equal(t, "waf_block", response["event_type"])
|
||||
assert.NotEmpty(t, response["timestamp"])
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeR6Intact tests that legacy PUT endpoint returns 410 Gone.
|
||||
// Constraint: Keep R6 410/no-mutation behavior intact.
|
||||
func TestSecurityEventIntakeR6Intact(t *testing.T) {
|
||||
// Create in-memory database with User table for this test
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(
|
||||
&models.User{},
|
||||
&models.NotificationProvider{},
|
||||
&models.NotificationConfig{},
|
||||
&models.Setting{},
|
||||
))
|
||||
|
||||
// Enable feature flag by default in tests
|
||||
featureFlag := &models.Setting{
|
||||
Key: "feature.notifications.security_provider_events.enabled",
|
||||
Value: "true",
|
||||
Type: "bool",
|
||||
Category: "feature",
|
||||
}
|
||||
require.NoError(t, db.Create(featureFlag).Error)
|
||||
|
||||
// Create an admin user for authentication
|
||||
adminUser := &models.User{
|
||||
Email: "admin@example.com",
|
||||
Name: "Admin User",
|
||||
PasswordHash: "$2a$10$abcdefghijklmnopqrstuvwxyz", // Dummy bcrypt hash
|
||||
Role: models.RoleAdmin,
|
||||
Enabled: true,
|
||||
}
|
||||
require.NoError(t, db.Create(adminUser).Error)
|
||||
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
handler := NewSecurityNotificationHandler(service)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
|
||||
// Add auth middleware that sets user context
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("user_id", adminUser.ID)
|
||||
c.Set("user", adminUser)
|
||||
c.Set("role", "admin")
|
||||
c.Next()
|
||||
})
|
||||
|
||||
router.PUT("/api/v1/notifications/settings/security", handler.UpdateSettings)
|
||||
|
||||
reqBody := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"security_waf_enabled": true,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("PUT", "/api/v1/notifications/settings/security", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusGone, w.Code, "PUT should return 410 Gone")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "legacy_security_settings_deprecated", response["error"])
|
||||
assert.Equal(t, "LEGACY_SECURITY_SETTINGS_DEPRECATED", response["code"])
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeDiscordOnly tests Discord-only dispatch enforcement.
|
||||
// Constraint: Keep Discord-only dispatch enforcement for this rollout stage.
|
||||
func TestSecurityEventIntakeDiscordOnly(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
// Create various provider types
|
||||
discordProvider := &models.NotificationProvider{
|
||||
Name: "Discord",
|
||||
Type: "discord",
|
||||
URL: "https://discord.com/api/webhooks/123/token",
|
||||
Enabled: true,
|
||||
NotifySecurityWAFBlocks: true,
|
||||
}
|
||||
require.NoError(t, db.Create(discordProvider).Error)
|
||||
|
||||
// Non-Discord providers should also work with supportsJSONTemplates
|
||||
webhookProvider := &models.NotificationProvider{
|
||||
Name: "Webhook",
|
||||
Type: "webhook",
|
||||
URL: "https://example.com/webhook",
|
||||
Enabled: true,
|
||||
NotifySecurityWAFBlocks: true,
|
||||
}
|
||||
require.NoError(t, db.Create(webhookProvider).Error)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"127.0.0.0/8"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
event := models.SecurityEvent{
|
||||
EventType: "waf_block",
|
||||
Severity: "error",
|
||||
Message: "WAF blocked request",
|
||||
ClientIP: "192.168.1.100",
|
||||
Path: "/admin",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
||||
c.Request.RemoteAddr = "127.0.0.1:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusAccepted, w.Code)
|
||||
|
||||
// Validate both Discord and webhook providers exist (JSON-capable)
|
||||
var providers []models.NotificationProvider
|
||||
err := db.Where("enabled = ?", true).Find(&providers).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, len(providers), "Both Discord and webhook providers should be enabled")
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeMalformedPayload tests rejection of malformed event payloads.
|
||||
func TestSecurityEventIntakeMalformedPayload(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"127.0.0.0/8"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
// Malformed JSON
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader([]byte("{invalid json")))
|
||||
c.Request.RemoteAddr = "127.0.0.1:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code, "Malformed JSON should be rejected")
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Invalid security event payload", response["error"])
|
||||
}
|
||||
|
||||
// TestSecurityEventIntakeIPv6Localhost tests that IPv6 localhost is accepted.
|
||||
func TestSecurityEventIntakeIPv6Localhost(t *testing.T) {
|
||||
db := SetupCompatibilityTestDB(t)
|
||||
|
||||
notificationService := services.NewNotificationService(db)
|
||||
service := services.NewEnhancedSecurityNotificationService(db)
|
||||
managementCIDRs := []string{"10.0.0.0/8"}
|
||||
|
||||
handler := NewSecurityNotificationHandlerWithDeps(
|
||||
service,
|
||||
nil,
|
||||
"",
|
||||
notificationService,
|
||||
managementCIDRs,
|
||||
)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
event := models.SecurityEvent{
|
||||
EventType: "acl_deny",
|
||||
Severity: "warn",
|
||||
Message: "ACL denied request",
|
||||
ClientIP: "::1",
|
||||
Path: "/api/admin",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
body, _ := json.Marshal(event)
|
||||
|
||||
// IPv6 localhost
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/security/events", bytes.NewReader(body))
|
||||
c.Request.RemoteAddr = "[::1]:12345"
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
handler.HandleSecurityEvent(c)
|
||||
|
||||
assert.Equal(t, http.StatusAccepted, w.Code, "IPv6 localhost should be accepted")
|
||||
}
|
||||
Reference in New Issue
Block a user