package handlers import ( "errors" "net/http" "github.com/gin-gonic/gin" crowdsec "git.netcell-it.de/projekte/edgeguard-native/internal/crowdsec" "git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response" "git.netcell-it.de/projekte/edgeguard-native/internal/services/audit" ) // CrowdSecHandler exposes the CrowdSec IDS/IPS management REST API: // // GET /crowdsec/status // GET /crowdsec/decisions // POST /crowdsec/decisions // DELETE /crowdsec/decisions (?ip= or ?id=) // GET /crowdsec/alerts // DELETE /crowdsec/alerts/:id // GET /crowdsec/bouncers // DELETE /crowdsec/bouncers/:name // GET /crowdsec/machines // DELETE /crowdsec/machines/:id // GET /crowdsec/collections // POST /crowdsec/collections/:name/install // DELETE /crowdsec/collections/:name type CrowdSecHandler struct { Audit *audit.Repo NodeID string } // NewCrowdSecHandler returns a CrowdSecHandler wired with audit and node-id. func NewCrowdSecHandler(a *audit.Repo, nodeID string) *CrowdSecHandler { return &CrowdSecHandler{Audit: a, NodeID: nodeID} } // Register mounts all CrowdSec routes onto the provided authenticated router // group. func (h *CrowdSecHandler) Register(rg *gin.RouterGroup) { g := rg.Group("/crowdsec") g.GET("/status", h.Status) g.GET("/decisions", h.ListDecisions) g.POST("/decisions", h.AddDecision) g.DELETE("/decisions", h.DeleteDecision) g.GET("/alerts", h.ListAlerts) g.DELETE("/alerts/:id", h.DeleteAlert) g.GET("/bouncers", h.ListBouncers) g.DELETE("/bouncers/:name", h.DeleteBouncer) g.GET("/machines", h.ListMachines) g.DELETE("/machines/:id", h.DeleteMachine) g.GET("/collections", h.ListCollections) g.POST("/collections/:name/install", h.InstallCollection) g.DELETE("/collections/:name", h.RemoveCollection) } // csNotInstalled responds with 503 when cscli is absent. func csNotInstalled(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "crowdsec not installed"}) } // ---------- Status ---------------------------------------------------------- // Status returns live status of the CrowdSec agent + bouncer. // Does NOT require cscli — uses systemctl for running-state checks. func (h *CrowdSecHandler) Status(c *gin.Context) { st := crowdsec.ServiceStatus(c.Request.Context()) response.OK(c, st) } // ---------- Decisions ------------------------------------------------------- // ListDecisions returns all active decisions. func (h *CrowdSecHandler) ListDecisions(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } list, err := crowdsec.Decisions(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"decisions": list}) } // addDecisionBody is the expected JSON body for POST /crowdsec/decisions. type addDecisionBody struct { IP string `json:"ip" binding:"required"` Duration string `json:"duration" binding:"required"` Reason string `json:"reason"` Type string `json:"type"` } // AddDecision creates a new ban/captcha decision. func (h *CrowdSecHandler) AddDecision(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } var body addDecisionBody if err := c.ShouldBindJSON(&body); err != nil { response.BadRequest(c, err) return } if body.Reason == "" { body.Reason = "manual ban" } if body.Type == "" { body.Type = "ban" } if err := crowdsec.AddDecision(c.Request.Context(), body.IP, body.Duration, body.Reason, body.Type); err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "crowdsec.decision.add", body.IP, gin.H{"duration": body.Duration, "type": body.Type, "reason": body.Reason}, h.NodeID) response.Created(c, gin.H{"ip": body.IP, "duration": body.Duration, "type": body.Type}) } // DeleteDecision removes a decision by IP (?ip=) or by ID (?id=). func (h *CrowdSecHandler) DeleteDecision(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } ip := c.Query("ip") id := c.Query("id") if ip == "" && id == "" { response.BadRequest(c, errors.New("query parameter 'ip' or 'id' required")) return } var err error var target string if ip != "" { err = crowdsec.DeleteDecisionByIP(c.Request.Context(), ip) target = ip } else { err = crowdsec.DeleteDecisionByID(c.Request.Context(), id) target = id } if err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "crowdsec.decision.delete", target, nil, h.NodeID) response.OK(c, gin.H{"deleted": target}) } // ---------- Alerts ---------------------------------------------------------- // ListAlerts returns recent CrowdSec alerts. func (h *CrowdSecHandler) ListAlerts(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } list, err := crowdsec.Alerts(c.Request.Context(), 200) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"alerts": list}) } // DeleteAlert discards a single alert. func (h *CrowdSecHandler) DeleteAlert(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } id := c.Param("id") if err := crowdsec.DeleteAlert(c.Request.Context(), id); err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"deleted": id}) } // ---------- Bouncers -------------------------------------------------------- // ListBouncers returns all registered bouncers. func (h *CrowdSecHandler) ListBouncers(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } list, err := crowdsec.Bouncers(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"bouncers": list}) } // DeleteBouncer removes a bouncer by name. func (h *CrowdSecHandler) DeleteBouncer(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } name := c.Param("name") if err := crowdsec.DeleteBouncer(c.Request.Context(), name); err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "crowdsec.bouncer.delete", name, nil, h.NodeID) response.OK(c, gin.H{"deleted": name}) } // ---------- Machines -------------------------------------------------------- // ListMachines returns all registered machines. func (h *CrowdSecHandler) ListMachines(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } list, err := crowdsec.Machines(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"machines": list}) } // DeleteMachine removes a machine by ID. func (h *CrowdSecHandler) DeleteMachine(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } id := c.Param("id") if err := crowdsec.DeleteMachine(c.Request.Context(), id); err != nil { response.Internal(c, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "crowdsec.machine.delete", id, nil, h.NodeID) response.OK(c, gin.H{"deleted": id}) } // ---------- Collections ----------------------------------------------------- // ListCollections returns all hub collections and their install status. func (h *CrowdSecHandler) ListCollections(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } list, err := crowdsec.Collections(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"collections": list}) } // InstallCollection installs a hub collection by name. func (h *CrowdSecHandler) InstallCollection(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } name := c.Param("name") if err := crowdsec.InstallCollection(c.Request.Context(), name); err != nil { response.Internal(c, err) return } response.Created(c, gin.H{"installed": name}) } // RemoveCollection removes a hub collection by name. func (h *CrowdSecHandler) RemoveCollection(c *gin.Context) { if !crowdsec.IsInstalled() { csNotInstalled(c) return } name := c.Param("name") if err := crowdsec.RemoveCollection(c.Request.Context(), name); err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"removed": name}) }