Skip to content

Commit b86afd3

Browse files
committed
enforce cscli list limits and add metrics UI
add a hard safety cap for cscli list commands (constants.MaxListLimit) and an ExecCommandTimeout. Introduce DecisionListLimit and AlertListLimit config options (env vars DECISION_LIST_LIMIT, ALERT_LIST_LIMIT) and an EffectiveLimit helper to compute the applied limit. Update dashboard handlers to pass a --limit argument to cscli (use strconv) to prevent huge result sets and potential memory exhaustion. Enhance the Metrics page (web/src/pages/Metrics.tsx) to render structured metrics as tabbed tables with flattening logic and improved UI/ordering.
1 parent e8ff222 commit b86afd3

5 files changed

Lines changed: 236 additions & 45 deletions

File tree

internal/api/handlers/dashboard.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/http"
7+
"strconv"
78
"strings"
89
"time"
910

1011
"crowdsec-manager/internal/cache"
1112
"crowdsec-manager/internal/config"
13+
"crowdsec-manager/internal/constants"
1214
"crowdsec-manager/internal/database"
1315
"crowdsec-manager/internal/docker"
1416
"crowdsec-manager/internal/logger"
@@ -45,9 +47,9 @@ func GetDecisions(dockerClient *docker.Client, cfg *config.Config, ttlCache ...*
4547

4648
logger.Info("Getting CrowdSec decisions via cscli")
4749

48-
output, err := dockerClient.ExecCommand(cfg.CrowdsecContainerName, []string{
49-
"cscli", "decisions", "list", "-o", "json",
50-
})
50+
limit := config.EffectiveLimit(cfg.DecisionListLimit, constants.MaxListLimit)
51+
cmd := []string{"cscli", "decisions", "list", "-o", "json", "--limit", strconv.Itoa(limit)}
52+
output, err := dockerClient.ExecCommand(cfg.CrowdsecContainerName, cmd)
5153
if err != nil {
5254
c.JSON(http.StatusInternalServerError, models.Response{
5355
Success: false,

internal/api/handlers/dashboard_analysis.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/http"
7+
"strconv"
78
"time"
89

910
"crowdsec-manager/internal/cache"
1011
"crowdsec-manager/internal/config"
12+
"crowdsec-manager/internal/constants"
1113
"crowdsec-manager/internal/docker"
1214
"crowdsec-manager/internal/logger"
1315
"crowdsec-manager/internal/models"
@@ -22,7 +24,8 @@ func GetDecisionsAnalysis(dockerClient *docker.Client, cfg *config.Config) gin.H
2224
dockerClient = resolveDockerClient(c, dockerClient)
2325
logger.Info("Getting CrowdSec decisions analysis via cscli")
2426

25-
cmd := []string{"cscli", "decisions", "list", "-o", "json"}
27+
limit := config.EffectiveLimit(cfg.DecisionListLimit, constants.MaxListLimit)
28+
cmd := []string{"cscli", "decisions", "list", "-o", "json", "--limit", strconv.Itoa(limit)}
2629

2730
// Add filters based on query parameters
2831
if v := c.Query("ip"); v != "" {
@@ -165,7 +168,8 @@ func GetAlertsAnalysis(dockerClient *docker.Client, cfg *config.Config, ttlCache
165168
if v := c.Query("id"); v != "" {
166169
cmd = []string{"cscli", "alerts", "inspect", v, "-o", "json"}
167170
} else {
168-
cmd = []string{"cscli", "alerts", "list", "-o", "json"}
171+
alertLimit := config.EffectiveLimit(cfg.AlertListLimit, constants.MaxListLimit)
172+
cmd = []string{"cscli", "alerts", "list", "-o", "json", "--limit", strconv.Itoa(alertLimit)}
169173

170174
// Add filters based on query parameters
171175
if v := c.Query("ip"); v != "" {

internal/config/config.go

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ type Config struct {
3535
CrowdSecAcquisFile string
3636

3737
// CrowdSec container-internal paths and URLs
38-
CrowdSecWhitelistPath string
39-
CrowdSecProfilesPath string
40-
CrowdSecNotificationsDir string
41-
CrowdSecScenariosDir string
42-
CrowdSecMetricsURL string
43-
CrowdSecConsoleURL string
44-
CrowdSecCTIURLPattern string
38+
CrowdSecWhitelistPath string
39+
CrowdSecProfilesPath string
40+
CrowdSecNotificationsDir string
41+
CrowdSecScenariosDir string
42+
CrowdSecMetricsURL string
43+
CrowdSecConsoleURL string
44+
CrowdSecCTIURLPattern string
4545

4646
// Traefik container-internal paths
4747
TraefikCaptchaHTMLPath string
@@ -60,14 +60,17 @@ type Config struct {
6060
GerbilContainerName string
6161
TraefikContainerName string
6262

63-
6463
// Services
6564
Services []string
6665
ServicesWithCrowdsec []string
6766
IncludeCrowdsec bool
6867
IncludePangolin bool
6968
IncludeGerbil bool
7069

70+
// CrowdSec list limits (0 = unlimited)
71+
DecisionListLimit int
72+
AlertListLimit int
73+
7174
// NATS Messaging (optional)
7275
NatsURL string
7376
NatsToken string
@@ -83,37 +86,37 @@ type Config struct {
8386
// It also creates required directories and dynamically builds service lists from compose file
8487
func Load() (*Config, error) {
8588
cfg := &Config{
86-
Port: getEnvAsInt("PORT", 8080),
87-
Environment: getEnv("ENVIRONMENT", "development"),
88-
LogLevel: getEnv("LOG_LEVEL", "info"),
89-
LogFile: getEnv("LOG_FILE", "./logs/crowdsec-manager.log"),
90-
DockerHost: getEnv("DOCKER_HOST", ""),
91-
DockerHosts: getEnv("DOCKER_HOSTS", ""),
92-
ComposeFile: getEnv("COMPOSE_FILE", "./docker-compose.yml"),
93-
PangolinDir: getEnv("PANGOLIN_DIR", "."),
94-
ConfigDir: getEnv("CONFIG_DIR", "./config"),
95-
DatabasePath: getEnv("DATABASE_PATH", "./data/settings.db"),
96-
TraefikDynamicConfig: getEnv("TRAEFIK_DYNAMIC_CONFIG", "/etc/traefik/dynamic_config.yml"),
97-
TraefikStaticConfig: getEnv("TRAEFIK_STATIC_CONFIG", "/etc/traefik/traefik_config.yml"),
98-
TraefikAccessLog: getEnv("TRAEFIK_ACCESS_LOG", "/var/log/traefik/access.log"),
99-
TraefikErrorLog: getEnv("TRAEFIK_ERROR_LOG", "/var/log/traefik/traefik.log"),
100-
CrowdSecAcquisFile: getEnv("CROWDSEC_ACQUIS_FILE", "/etc/crowdsec/acquis.yaml"),
101-
CrowdSecWhitelistPath: getEnv("CROWDSEC_WHITELIST_PATH", "/etc/crowdsec/parsers/s02-enrich/mywhitelists.yaml"),
102-
CrowdSecProfilesPath: getEnv("CROWDSEC_PROFILES_PATH", "/etc/crowdsec/profiles.yaml"),
103-
CrowdSecNotificationsDir: getEnv("CROWDSEC_NOTIFICATIONS_DIR", "/etc/crowdsec/notifications"),
104-
CrowdSecScenariosDir: getEnv("CROWDSEC_SCENARIOS_DIR", "/etc/crowdsec/scenarios"),
105-
CrowdSecMetricsURL: getEnv("CROWDSEC_METRICS_URL", "http://localhost:6060/metrics"),
106-
CrowdSecConsoleURL: getEnv("CROWDSEC_CONSOLE_URL", "https://app.crowdsec.net/"),
107-
CrowdSecCTIURLPattern: getEnv("CROWDSEC_CTI_URL_PATTERN", "https://app.crowdsec.net/cti/{{.Value}}"),
108-
TraefikCaptchaHTMLPath: getEnv("TRAEFIK_CAPTCHA_HTML_PATH", "/etc/traefik/conf/captcha.html"),
109-
TraefikCaptchaEnvPath: getEnv("TRAEFIK_CAPTCHA_ENV_PATH", "/etc/traefik/captcha.env"),
89+
Port: getEnvAsInt("PORT", 8080),
90+
Environment: getEnv("ENVIRONMENT", "development"),
91+
LogLevel: getEnv("LOG_LEVEL", "info"),
92+
LogFile: getEnv("LOG_FILE", "./logs/crowdsec-manager.log"),
93+
DockerHost: getEnv("DOCKER_HOST", ""),
94+
DockerHosts: getEnv("DOCKER_HOSTS", ""),
95+
ComposeFile: getEnv("COMPOSE_FILE", "./docker-compose.yml"),
96+
PangolinDir: getEnv("PANGOLIN_DIR", "."),
97+
ConfigDir: getEnv("CONFIG_DIR", "./config"),
98+
DatabasePath: getEnv("DATABASE_PATH", "./data/settings.db"),
99+
TraefikDynamicConfig: getEnv("TRAEFIK_DYNAMIC_CONFIG", "/etc/traefik/dynamic_config.yml"),
100+
TraefikStaticConfig: getEnv("TRAEFIK_STATIC_CONFIG", "/etc/traefik/traefik_config.yml"),
101+
TraefikAccessLog: getEnv("TRAEFIK_ACCESS_LOG", "/var/log/traefik/access.log"),
102+
TraefikErrorLog: getEnv("TRAEFIK_ERROR_LOG", "/var/log/traefik/traefik.log"),
103+
CrowdSecAcquisFile: getEnv("CROWDSEC_ACQUIS_FILE", "/etc/crowdsec/acquis.yaml"),
104+
CrowdSecWhitelistPath: getEnv("CROWDSEC_WHITELIST_PATH", "/etc/crowdsec/parsers/s02-enrich/mywhitelists.yaml"),
105+
CrowdSecProfilesPath: getEnv("CROWDSEC_PROFILES_PATH", "/etc/crowdsec/profiles.yaml"),
106+
CrowdSecNotificationsDir: getEnv("CROWDSEC_NOTIFICATIONS_DIR", "/etc/crowdsec/notifications"),
107+
CrowdSecScenariosDir: getEnv("CROWDSEC_SCENARIOS_DIR", "/etc/crowdsec/scenarios"),
108+
CrowdSecMetricsURL: getEnv("CROWDSEC_METRICS_URL", "http://localhost:6060/metrics"),
109+
CrowdSecConsoleURL: getEnv("CROWDSEC_CONSOLE_URL", "https://app.crowdsec.net/"),
110+
CrowdSecCTIURLPattern: getEnv("CROWDSEC_CTI_URL_PATTERN", "https://app.crowdsec.net/cti/{{.Value}}"),
111+
TraefikCaptchaHTMLPath: getEnv("TRAEFIK_CAPTCHA_HTML_PATH", "/etc/traefik/conf/captcha.html"),
112+
TraefikCaptchaEnvPath: getEnv("TRAEFIK_CAPTCHA_ENV_PATH", "/etc/traefik/captcha.env"),
110113
TraefikDynamicConfigSearch: []string{
111114
"/etc/traefik/config/dynamic_config.yml",
112115
"/etc/traefik/dynamic_config.yaml",
113116
"/etc/traefik/config/dynamic_config.yaml",
114117
},
115-
CaptchaGracePeriod: getEnvAsInt("CAPTCHA_GRACE_PERIOD", 1800),
116-
BackupDir: getEnv("BACKUP_DIR", "./backups"),
118+
CaptchaGracePeriod: getEnvAsInt("CAPTCHA_GRACE_PERIOD", 1800),
119+
BackupDir: getEnv("BACKUP_DIR", "./backups"),
117120
RetentionDays: getEnvAsInt("RETENTION_DAYS", 60),
118121
BackupItems: []string{"docker-compose.yml", "config"},
119122
CrowdsecContainerName: getEnv("CROWDSEC_CONTAINER_NAME", "crowdsec"),
@@ -123,6 +126,8 @@ func Load() (*Config, error) {
123126
IncludeCrowdsec: getEnvAsBool("INCLUDE_CROWDSEC", true),
124127
IncludePangolin: getEnvAsBool("INCLUDE_PANGOLIN", true),
125128
IncludeGerbil: getEnvAsBool("INCLUDE_GERBIL", true),
129+
DecisionListLimit: getEnvAsInt("DECISION_LIST_LIMIT", 200),
130+
AlertListLimit: getEnvAsInt("ALERT_LIST_LIMIT", 200),
126131
NatsURL: getEnv("NATS_URL", ""),
127132
NatsToken: getEnv("NATS_TOKEN", ""),
128133
NatsEnabled: getEnvAsBool("NATS_ENABLED", false),
@@ -212,6 +217,16 @@ func (c *Config) GetServices() []string {
212217
return c.Services
213218
}
214219

220+
// EffectiveLimit returns the effective list limit, applying the hard safety cap.
221+
// If the configured limit is 0 (unlimited), it returns maxLimit.
222+
// If the configured limit exceeds maxLimit, it returns maxLimit.
223+
func EffectiveLimit(configured, maxLimit int) int {
224+
if configured <= 0 || configured > maxLimit {
225+
return maxLimit
226+
}
227+
return configured
228+
}
229+
215230
// getEnv retrieves an environment variable or returns the default value if not set
216231
func getEnv(key, defaultValue string) string {
217232
if value := os.Getenv(key); value != "" {

internal/constants/constants.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ const CrowdSecConfigSubdir = "crowdsec"
3030
// DefaultWebSocketBufferSize is the default buffer size for WebSocket read/write operations
3131
const DefaultWebSocketBufferSize = 4096
3232

33+
// MaxListLimit is the hard safety cap for cscli list commands.
34+
// Even when the user sets limit=0 (unlimited), we enforce this cap to prevent
35+
// memory exhaustion from very large result sets (e.g. 10,000+ decisions/alerts).
36+
// Matches CrowdSec's default DB retention of 5,000 items.
37+
const MaxListLimit = 5000
38+
39+
// ExecCommandTimeout is the timeout for cscli commands executed inside containers.
40+
// Prevents hanging if cscli takes too long on large datasets.
41+
const ExecCommandTimeout = 60 * time.Second
42+
3343
// ExternalHTTPTimeout is the timeout for outbound HTTP requests (IP lookups, etc.)
3444
const ExternalHTTPTimeout = 10 * time.Second
3545

0 commit comments

Comments
 (0)