- Backend: internal/crowdsec/service.go — vollständige cscli-Wrapper (Decisions, Alerts, Bouncers, Machines, Collections, ServiceStatus) - Handler: 12 REST-Endpoints mit Audit-Logging unter /crowdsec/* - Migration 0036: crowdsec_settings-Tabelle - postinst: CrowdSec-Auto-Install (crowdsec + crowdsec-firewall-bouncer-nftables) inkl. sudoers-Einträge für alle cscli-Operationen - systemd: /var/lib/crowdsec in ReadWritePaths - UI: CrowdSec-Page mit StatusStrip + 5 Tabs (Decisions, Alerts, Bouncers, Machines, Collections), Sidebar-Eintrag, i18n EN+DE - firewall: flush ruleset → flush table inet edgeguard (CrowdSec-nftables-Table bleibt bei Firewall-Render erhalten) - cluster: Firewall-Reload nur bei echter IP-Änderung, nicht bei jedem periodischen Secondary-Heartbeat (verhindert nftables-Counter-Reset) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
286 lines
8.0 KiB
Go
286 lines
8.0 KiB
Go
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=<ip> or ?id=<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})
|
|
}
|