package v1 import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/gorilla/mux" "github.com/wild-cloud/wild-central/internal/crowdsec" ) // CrowdSecManager is the interface for CrowdSec operations used by these handlers. type CrowdSecManager interface { GetStatus() (*crowdsec.Status, error) GetBanSummary() (*crowdsec.BanSummary, error) GetDecisions() ([]crowdsec.Decision, error) AddDecision(ip, decType, reason, duration string) error DeleteDecision(id int) error DeleteDecisionByIP(ip string) error GetAlerts(limit int) ([]crowdsec.Alert, error) GetMachines() ([]crowdsec.Machine, error) AddMachine(name, password string) error DeleteMachine(name string) error GetBouncers() ([]crowdsec.Bouncer, error) AddBouncer(name, apiKey string) error DeleteBouncer(name string) error } // crowdsecHandlers groups CrowdSec HTTP handlers with their single dependency. type crowdsecHandlers struct { crowdsec CrowdSecManager } // CrowdSecStatus returns whether CrowdSec is running on Wild Central // and lists registered machines and bouncers. func (h *crowdsecHandlers) Status(w http.ResponseWriter, r *http.Request) { status, err := h.crowdsec.GetStatus() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get CrowdSec status: %v", err)) return } respondJSON(w, http.StatusOK, status) } // GetSummary returns aggregated ban counts by scenario across all active decisions. func (h *crowdsecHandlers) GetSummary(w http.ResponseWriter, r *http.Request) { summary, err := h.crowdsec.GetBanSummary() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get ban summary: %v", err)) return } respondJSON(w, http.StatusOK, summary) } // GetDecisions returns active ban/captcha decisions func (h *crowdsecHandlers) GetDecisions(w http.ResponseWriter, r *http.Request) { decisions, err := h.crowdsec.GetDecisions() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get decisions: %v", err)) return } respondJSON(w, http.StatusOK, map[string]any{"decisions": decisions}) } // DeleteDecision removes a ban decision by numeric ID func (h *crowdsecHandlers) DeleteDecision(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { respondError(w, http.StatusBadRequest, "invalid decision ID") return } if err := h.crowdsec.DeleteDecision(id); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete decision: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{"message": "Decision deleted"}) } // AddDecision adds a manual ban or allow decision for an IP. func (h *crowdsecHandlers) AddDecision(w http.ResponseWriter, r *http.Request) { var req struct { IP string `json:"ip"` Type string `json:"type"` Reason string `json:"reason"` Duration string `json:"duration"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } if req.IP == "" { respondError(w, http.StatusBadRequest, "ip is required") return } if req.Type != "ban" && req.Type != "allow" { respondError(w, http.StatusBadRequest, "type must be 'ban' or 'allow'") return } if req.Reason == "" { req.Reason = "manual" } if req.Duration == "" { req.Duration = "24h" } if err := h.crowdsec.AddDecision(req.IP, req.Type, req.Reason, req.Duration); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add decision: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{"message": fmt.Sprintf("Decision added: %s %s for %s", req.Type, req.IP, req.Duration)}) } // DeleteDecisionByIP removes all decisions for a specific IP address func (h *crowdsecHandlers) DeleteDecisionByIP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) ip := vars["ip"] if ip == "" { respondError(w, http.StatusBadRequest, "ip is required") return } if err := h.crowdsec.DeleteDecisionByIP(ip); err != nil { // cscli exits non-zero when there's nothing to delete — treat as success if strings.Contains(err.Error(), "no decision") || strings.Contains(err.Error(), "0 decision") { respondJSON(w, http.StatusOK, map[string]string{"message": "No active decision for " + ip}) return } respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete decision: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{"message": "Decisions removed for " + ip}) } // GetAlerts returns recent detection events from registered agents. func (h *crowdsecHandlers) GetAlerts(w http.ResponseWriter, r *http.Request) { limit := 50 if l := r.URL.Query().Get("limit"); l != "" { if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 200 { limit = n } } alerts, err := h.crowdsec.GetAlerts(limit) if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get alerts: %v", err)) return } respondJSON(w, http.StatusOK, map[string]any{"alerts": alerts}) } // GetMachines returns all registered CrowdSec agent machines func (h *crowdsecHandlers) GetMachines(w http.ResponseWriter, r *http.Request) { machines, err := h.crowdsec.GetMachines() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get machines: %v", err)) return } respondJSON(w, http.StatusOK, map[string]any{"machines": machines}) } // DeleteMachine removes a registered machine func (h *crowdsecHandlers) DeleteMachine(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["name"] if err := h.crowdsec.DeleteMachine(name); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete machine: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{"message": "Machine deleted"}) } // GetBouncers returns all registered bouncers func (h *crowdsecHandlers) GetBouncers(w http.ResponseWriter, r *http.Request) { bouncers, err := h.crowdsec.GetBouncers() if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get bouncers: %v", err)) return } respondJSON(w, http.StatusOK, map[string]any{"bouncers": bouncers}) } // DeleteBouncer removes a registered bouncer func (h *crowdsecHandlers) DeleteBouncer(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["name"] if err := h.crowdsec.DeleteBouncer(name); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete bouncer: %v", err)) return } respondJSON(w, http.StatusOK, map[string]string{"message": "Bouncer deleted"}) } // AddMachine registers a new CrowdSec agent machine with pre-generated credentials. func (h *crowdsecHandlers) AddMachine(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.Password == "" { respondError(w, http.StatusBadRequest, "name and password are required") return } if err := h.crowdsec.AddMachine(req.Name, req.Password); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add machine: %v", err)) return } respondJSON(w, http.StatusCreated, map[string]string{"message": fmt.Sprintf("Machine %s registered", req.Name)}) } // AddBouncer registers a new bouncer with a pre-generated API key. func (h *crowdsecHandlers) AddBouncer(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` APIKey string `json:"apiKey"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } if req.Name == "" || req.APIKey == "" { respondError(w, http.StatusBadRequest, "name and apiKey are required") return } if err := h.crowdsec.AddBouncer(req.Name, req.APIKey); err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add bouncer: %v", err)) return } respondJSON(w, http.StatusCreated, map[string]string{"message": fmt.Sprintf("Bouncer %s registered", req.Name)}) }