package server import ( "net" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "warpbox/lib/activity" "warpbox/lib/alerts" "warpbox/lib/security" ) type adminAlertsActionRequest struct { Action string `json:"action"` IDs []string `json:"ids"` } type adminSecurityActionRequest struct { Action string `json:"action"` IP string `json:"ip"` BanUntil string `json:"ban_until"` } func (app *App) reloadSecurityConfig() { if app.securityGuard == nil { app.securityGuard = security.NewGuard() } app.securityGuard.Reload(security.Config{ IPWhitelist: app.config.SecurityIPWhitelist, AdminIPWhitelist: app.config.SecurityAdminIPWhitelist, LoginWindowSeconds: app.config.SecurityLoginWindowSeconds, LoginMaxAttempts: app.config.SecurityLoginMaxAttempts, BanSeconds: app.config.SecurityBanSeconds, ScanWindowSeconds: app.config.SecurityScanWindowSeconds, ScanMaxAttempts: app.config.SecurityScanMaxAttempts, UploadWindowSeconds: app.config.SecurityUploadWindowSeconds, UploadMaxRequests: app.config.SecurityUploadMaxRequests, UploadMaxBytes: app.config.SecurityUploadMaxBytes, }) } func (app *App) logActivity(kind string, severity string, message string, ctx *gin.Context, meta map[string]string) { if app.activityStore == nil { return } event := activity.Event{ Kind: kind, Severity: severity, Message: message, CreatedAt: time.Now().UTC(), Meta: meta, } if ctx != nil { event.IP = clientIP(ctx) event.Path = ctx.Request.URL.Path event.Method = ctx.Request.Method } _ = app.activityStore.Append(event, app.config.ActivityRetentionSeconds) } func (app *App) createAlert(title string, severity string, group string, code string, trace string, message string, meta map[string]string) { if app.alertStore == nil { return } _ = app.alertStore.Add(alerts.Alert{ Title: title, Severity: severity, Group: group, Code: code, Trace: trace, Message: message, Status: alerts.StatusOpen, Meta: meta, }) } func (app *App) securityMiddleware() gin.HandlerFunc { return func(ctx *gin.Context) { if app.securityGuard == nil { ctx.Next() return } ip := clientIP(ctx) if app.securityGuard.IsWhitelisted(ip) || app.securityGuard.IsAdminWhitelisted(ip) { ctx.Next() return } if app.securityGuard.IsBanned(ip) { app.logActivity("security.block", "high", "Blocked banned IP", ctx, nil) ctx.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "Too many abusive requests. Try again later."}) return } ctx.Next() } } func (app *App) handleNoRoute(ctx *gin.Context) { if app.securityGuard == nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) return } path := strings.ToLower(ctx.Request.URL.Path) suspicious := strings.Contains(path, "../") || strings.Contains(path, ".php") || strings.Contains(path, "wp-admin") || strings.Contains(path, ".env") if suspicious { ip := clientIP(ctx) if !app.securityGuard.IsWhitelisted(ip) { banned, attempts := app.securityGuard.RegisterScanAttempt(ip, app.config.SecurityScanWindowSeconds, app.config.SecurityScanMaxAttempts, app.config.SecurityBanSeconds) app.logActivity("security.scan", "medium", "Suspicious path probe detected", ctx, map[string]string{"attempts": intToString(attempts)}) if banned { app.createAlert("IP auto-banned after malicious path scans", "high", "security", "410", "security.scan.autoban", "Repeated malicious path scans triggered temporary ban.", map[string]string{"ip": ip, "attempts": intToString(attempts)}) app.logActivity("security.ban", "high", "IP auto-banned after scans", ctx, map[string]string{"attempts": intToString(attempts)}) } } } ctx.JSON(http.StatusNotFound, gin.H{"error": "Not found"}) } func (app *App) handleAdminActivity(ctx *gin.Context) { if app.activityStore == nil { ctx.HTML(http.StatusOK, "admin/activity.html", gin.H{ "AdminUsername": app.config.AdminUsername, "AdminEmail": app.config.AdminEmail, "ActivePage": "activity", "Events": []activity.Event{}, }) return } events, err := app.activityStore.List(400, app.config.ActivityRetentionSeconds) if err != nil { ctx.String(http.StatusInternalServerError, "Could not load activity") return } ctx.HTML(http.StatusOK, "admin/activity.html", gin.H{ "AdminUsername": app.config.AdminUsername, "AdminEmail": app.config.AdminEmail, "ActivePage": "activity", "Events": events, }) } func (app *App) handleAdminSecurity(ctx *gin.Context) { events := []activity.Event{} alertsList := []alerts.Alert{} if app.activityStore != nil { events, _ = app.activityStore.List(100, app.config.ActivityRetentionSeconds) } if app.alertStore != nil { alertsList, _ = app.alertStore.List(50) } bans := []security.BanEntry{} if app.securityGuard != nil { bans = app.securityGuard.BanList() } ctx.HTML(http.StatusOK, "admin/security.html", gin.H{ "AdminUsername": app.config.AdminUsername, "AdminEmail": app.config.AdminEmail, "ActivePage": "security", "Events": events, "Alerts": alertsList, "Bans": bans, }) } func (app *App) handleAdminAlertsAction(ctx *gin.Context) { if app.alertStore == nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Alert store unavailable"}) return } var request adminAlertsActionRequest if err := ctx.ShouldBindJSON(&request); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action payload"}) return } switch request.Action { case "ack": if err := app.alertStore.SetStatus(request.IDs, alerts.StatusAcked); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not update alerts"}) return } case "close": if err := app.alertStore.SetStatus(request.IDs, alerts.StatusClosed); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not update alerts"}) return } case "delete": if err := app.alertStore.Delete(request.IDs); err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Could not delete alerts"}) return } default: ctx.JSON(http.StatusBadRequest, gin.H{"error": "Unknown action"}) return } app.logActivity("alerts.action", "low", "Admin changed alert state", ctx, map[string]string{"action": request.Action, "count": intToString(len(request.IDs))}) alertsList, _ := app.alertStore.List(500) ctx.JSON(http.StatusOK, gin.H{"ok": true, "alerts": alertsList}) } func (app *App) handleAdminSecurityAction(ctx *gin.Context) { if app.securityGuard == nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Security guard unavailable"}) return } var request adminSecurityActionRequest if err := ctx.ShouldBindJSON(&request); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action payload"}) return } ip := strings.TrimSpace(request.IP) if ip != "" && net.ParseIP(ip) == nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid IP"}) return } switch request.Action { case "ban": if ip == "" { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Missing IP"}) return } app.securityGuard.Ban(ip, app.config.SecurityBanSeconds) app.logActivity("security.manual_ban", "high", "Admin banned IP", ctx, map[string]string{"ip": ip}) app.createAlert("IP manually banned by admin", "medium", "security", "420", "security.manual.ban", "Admin manually applied temporary ban.", map[string]string{"ip": ip}) ctx.JSON(http.StatusOK, gin.H{"ok": true, "message": "IP banned", "bans": app.securityGuard.BanList()}) case "ban_until": if ip == "" { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Missing IP"}) return } until, err := time.Parse(time.RFC3339, strings.TrimSpace(request.BanUntil)) if err != nil || until.Before(time.Now().UTC()) { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ban expiration"}) return } app.securityGuard.BanUntil(ip, until) app.logActivity("security.manual_ban_until", "high", "Admin set custom ban expiration", ctx, map[string]string{"ip": ip, "until": until.UTC().Format(time.RFC3339)}) app.createAlert("Custom IP ban applied by admin", "medium", "security", "421", "security.manual.ban_until", "Admin set explicit ban expiration date.", map[string]string{"ip": ip, "until": until.UTC().Format(time.RFC3339)}) ctx.JSON(http.StatusOK, gin.H{"ok": true, "message": "IP ban expiration updated", "bans": app.securityGuard.BanList()}) case "unban": if ip == "" { ctx.JSON(http.StatusBadRequest, gin.H{"error": "Missing IP"}) return } app.securityGuard.Unban(ip) app.logActivity("security.manual_unban", "medium", "Admin unbanned IP", ctx, map[string]string{"ip": ip}) app.createAlert("IP unbanned by admin", "low", "security", "422", "security.manual.unban", "Admin manually removed temporary ban.", map[string]string{"ip": ip}) ctx.JSON(http.StatusOK, gin.H{"ok": true, "message": "IP unbanned", "bans": app.securityGuard.BanList()}) default: ctx.JSON(http.StatusBadRequest, gin.H{"error": "Unknown action"}) } } func intToString(value int) string { return strconv.Itoa(value) }