mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
First steps to implement a advanced-actions function to block recurring offenders before fail2ban
This commit is contained in:
@@ -47,24 +47,25 @@ func intFromNull(ni sql.NullInt64) int {
|
||||
}
|
||||
|
||||
type AppSettingsRecord struct {
|
||||
Language string
|
||||
Port int
|
||||
Debug bool
|
||||
CallbackURL string
|
||||
RestartNeeded bool
|
||||
AlertCountriesJSON string
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
SMTPUsername string
|
||||
SMTPPassword string
|
||||
SMTPFrom string
|
||||
SMTPUseTLS bool
|
||||
BantimeIncrement bool
|
||||
IgnoreIP string
|
||||
Bantime string
|
||||
Findtime string
|
||||
MaxRetry int
|
||||
DestEmail string
|
||||
Language string
|
||||
Port int
|
||||
Debug bool
|
||||
CallbackURL string
|
||||
RestartNeeded bool
|
||||
AlertCountriesJSON string
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
SMTPUsername string
|
||||
SMTPPassword string
|
||||
SMTPFrom string
|
||||
SMTPUseTLS bool
|
||||
BantimeIncrement bool
|
||||
IgnoreIP string
|
||||
Bantime string
|
||||
Findtime string
|
||||
MaxRetry int
|
||||
DestEmail string
|
||||
AdvancedActionsJSON string
|
||||
}
|
||||
|
||||
type ServerRecord struct {
|
||||
@@ -112,6 +113,18 @@ type RecurringIPStat struct {
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
}
|
||||
|
||||
type PermanentBlockRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Integration string `json:"integration"`
|
||||
Status string `json:"status"`
|
||||
Details string `json:"details"`
|
||||
Message string `json:"message"`
|
||||
ServerID string `json:"serverId"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// Init initializes the internal storage. Safe to call multiple times.
|
||||
func Init(dbPath string) error {
|
||||
initOnce.Do(func() {
|
||||
@@ -154,17 +167,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) {
|
||||
}
|
||||
|
||||
row := db.QueryRowContext(ctx, `
|
||||
SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail
|
||||
SELECT language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail, advanced_actions
|
||||
FROM app_settings
|
||||
WHERE id = 1`)
|
||||
|
||||
var (
|
||||
lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail sql.NullString
|
||||
port, smtpPort, maxretry sql.NullInt64
|
||||
debug, restartNeeded, smtpTLS, bantimeInc sql.NullInt64
|
||||
lang, callback, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, advancedActions sql.NullString
|
||||
port, smtpPort, maxretry sql.NullInt64
|
||||
debug, restartNeeded, smtpTLS, bantimeInc sql.NullInt64
|
||||
)
|
||||
|
||||
err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &ignoreIP, &bantime, &findtime, &maxretry, &destemail)
|
||||
err := row.Scan(&lang, &port, &debug, &callback, &restartNeeded, &alerts, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &advancedActions)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return AppSettingsRecord{}, false, nil
|
||||
}
|
||||
@@ -173,24 +186,25 @@ WHERE id = 1`)
|
||||
}
|
||||
|
||||
rec := AppSettingsRecord{
|
||||
Language: stringFromNull(lang),
|
||||
Port: intFromNull(port),
|
||||
Debug: intToBool(intFromNull(debug)),
|
||||
CallbackURL: stringFromNull(callback),
|
||||
RestartNeeded: intToBool(intFromNull(restartNeeded)),
|
||||
AlertCountriesJSON: stringFromNull(alerts),
|
||||
SMTPHost: stringFromNull(smtpHost),
|
||||
SMTPPort: intFromNull(smtpPort),
|
||||
SMTPUsername: stringFromNull(smtpUser),
|
||||
SMTPPassword: stringFromNull(smtpPass),
|
||||
SMTPFrom: stringFromNull(smtpFrom),
|
||||
SMTPUseTLS: intToBool(intFromNull(smtpTLS)),
|
||||
BantimeIncrement: intToBool(intFromNull(bantimeInc)),
|
||||
IgnoreIP: stringFromNull(ignoreIP),
|
||||
Bantime: stringFromNull(bantime),
|
||||
Findtime: stringFromNull(findtime),
|
||||
MaxRetry: intFromNull(maxretry),
|
||||
DestEmail: stringFromNull(destemail),
|
||||
Language: stringFromNull(lang),
|
||||
Port: intFromNull(port),
|
||||
Debug: intToBool(intFromNull(debug)),
|
||||
CallbackURL: stringFromNull(callback),
|
||||
RestartNeeded: intToBool(intFromNull(restartNeeded)),
|
||||
AlertCountriesJSON: stringFromNull(alerts),
|
||||
SMTPHost: stringFromNull(smtpHost),
|
||||
SMTPPort: intFromNull(smtpPort),
|
||||
SMTPUsername: stringFromNull(smtpUser),
|
||||
SMTPPassword: stringFromNull(smtpPass),
|
||||
SMTPFrom: stringFromNull(smtpFrom),
|
||||
SMTPUseTLS: intToBool(intFromNull(smtpTLS)),
|
||||
BantimeIncrement: intToBool(intFromNull(bantimeInc)),
|
||||
IgnoreIP: stringFromNull(ignoreIP),
|
||||
Bantime: stringFromNull(bantime),
|
||||
Findtime: stringFromNull(findtime),
|
||||
MaxRetry: intFromNull(maxretry),
|
||||
DestEmail: stringFromNull(destemail),
|
||||
AdvancedActionsJSON: stringFromNull(advancedActions),
|
||||
}
|
||||
|
||||
return rec, true, nil
|
||||
@@ -202,9 +216,9 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error {
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `
|
||||
INSERT INTO app_settings (
|
||||
id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail
|
||||
id, language, port, debug, callback_url, restart_needed, alert_countries, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, ignore_ip, bantime, findtime, maxretry, destemail, advanced_actions
|
||||
) VALUES (
|
||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
) ON CONFLICT(id) DO UPDATE SET
|
||||
language = excluded.language,
|
||||
port = excluded.port,
|
||||
@@ -223,7 +237,8 @@ INSERT INTO app_settings (
|
||||
bantime = excluded.bantime,
|
||||
findtime = excluded.findtime,
|
||||
maxretry = excluded.maxretry,
|
||||
destemail = excluded.destemail
|
||||
destemail = excluded.destemail,
|
||||
advanced_actions = excluded.advanced_actions
|
||||
`, rec.Language,
|
||||
rec.Port,
|
||||
boolToInt(rec.Debug),
|
||||
@@ -242,6 +257,7 @@ INSERT INTO app_settings (
|
||||
rec.Findtime,
|
||||
rec.MaxRetry,
|
||||
rec.DestEmail,
|
||||
rec.AdvancedActionsJSON,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -566,6 +582,33 @@ WHERE 1=1`
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// CountBanEventsByIP returns total number of ban events for a specific IP and optional server.
|
||||
func CountBanEventsByIP(ctx context.Context, ip, serverID string) (int64, error) {
|
||||
if db == nil {
|
||||
return 0, errors.New("storage not initialised")
|
||||
}
|
||||
if ip == "" {
|
||||
return 0, errors.New("ip is required")
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT COUNT(*)
|
||||
FROM ban_events
|
||||
WHERE ip = ?`
|
||||
args := []any{ip}
|
||||
|
||||
if serverID != "" {
|
||||
query += " AND server_id = ?"
|
||||
args = append(args, serverID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := db.QueryRowContext(ctx, query, args...).Scan(&total); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
// CountBanEventsByCountry returns aggregation per country code, optionally filtered by server.
|
||||
func CountBanEventsByCountry(ctx context.Context, since time.Time, serverID string) (map[string]int64, error) {
|
||||
if db == nil {
|
||||
@@ -695,7 +738,8 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
||||
bantime TEXT,
|
||||
findtime TEXT,
|
||||
maxretry INTEGER,
|
||||
destemail TEXT
|
||||
destemail TEXT,
|
||||
advanced_actions TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS servers (
|
||||
@@ -736,6 +780,21 @@ CREATE TABLE IF NOT EXISTS ban_events (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ban_events_server_id ON ban_events(server_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ban_events_occurred_at ON ban_events(occurred_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS permanent_blocks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip TEXT NOT NULL,
|
||||
integration TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
details TEXT,
|
||||
message TEXT,
|
||||
server_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(ip, integration)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status);
|
||||
`
|
||||
|
||||
if _, err := db.ExecContext(ctx, createTable); err != nil {
|
||||
@@ -749,6 +808,12 @@ CREATE INDEX IF NOT EXISTS idx_ban_events_occurred_at ON ban_events(occurred_at)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN advanced_actions TEXT`); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -762,3 +827,128 @@ func ensureDirectory(path string) error {
|
||||
}
|
||||
return os.MkdirAll(dir, 0o755)
|
||||
}
|
||||
|
||||
// UpsertPermanentBlock records or updates a permanent block entry.
|
||||
func UpsertPermanentBlock(ctx context.Context, rec PermanentBlockRecord) error {
|
||||
if db == nil {
|
||||
return errors.New("storage not initialised")
|
||||
}
|
||||
if rec.IP == "" || rec.Integration == "" {
|
||||
return errors.New("ip and integration are required")
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if rec.CreatedAt.IsZero() {
|
||||
rec.CreatedAt = now
|
||||
}
|
||||
rec.UpdatedAt = now
|
||||
if rec.Status == "" {
|
||||
rec.Status = "blocked"
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO permanent_blocks (ip, integration, status, details, message, server_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(ip, integration) DO UPDATE SET
|
||||
status = excluded.status,
|
||||
details = excluded.details,
|
||||
message = excluded.message,
|
||||
server_id = excluded.server_id,
|
||||
updated_at = excluded.updated_at`
|
||||
|
||||
_, err := db.ExecContext(ctx, query,
|
||||
rec.IP,
|
||||
rec.Integration,
|
||||
rec.Status,
|
||||
rec.Details,
|
||||
rec.Message,
|
||||
rec.ServerID,
|
||||
rec.CreatedAt.Format(time.RFC3339Nano),
|
||||
rec.UpdatedAt.Format(time.RFC3339Nano),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPermanentBlock retrieves a permanent block entry.
|
||||
func GetPermanentBlock(ctx context.Context, ip, integration string) (PermanentBlockRecord, bool, error) {
|
||||
if db == nil {
|
||||
return PermanentBlockRecord{}, false, errors.New("storage not initialised")
|
||||
}
|
||||
if ip == "" || integration == "" {
|
||||
return PermanentBlockRecord{}, false, errors.New("ip and integration are required")
|
||||
}
|
||||
|
||||
row := db.QueryRowContext(ctx, `
|
||||
SELECT id, ip, integration, status, details, message, server_id, created_at, updated_at
|
||||
FROM permanent_blocks
|
||||
WHERE ip = ? AND integration = ?`, ip, integration)
|
||||
|
||||
var rec PermanentBlockRecord
|
||||
var createdAt, updatedAt sql.NullString
|
||||
if err := row.Scan(&rec.ID, &rec.IP, &rec.Integration, &rec.Status, &rec.Details, &rec.Message, &rec.ServerID, &createdAt, &updatedAt); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return PermanentBlockRecord{}, false, nil
|
||||
}
|
||||
return PermanentBlockRecord{}, false, err
|
||||
}
|
||||
if createdAt.Valid {
|
||||
if ts, err := time.Parse(time.RFC3339Nano, createdAt.String); err == nil {
|
||||
rec.CreatedAt = ts
|
||||
}
|
||||
}
|
||||
if updatedAt.Valid {
|
||||
if ts, err := time.Parse(time.RFC3339Nano, updatedAt.String); err == nil {
|
||||
rec.UpdatedAt = ts
|
||||
}
|
||||
}
|
||||
return rec, true, nil
|
||||
}
|
||||
|
||||
// ListPermanentBlocks returns recent permanent block entries.
|
||||
func ListPermanentBlocks(ctx context.Context, limit int) ([]PermanentBlockRecord, error) {
|
||||
if db == nil {
|
||||
return nil, errors.New("storage not initialised")
|
||||
}
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT id, ip, integration, status, details, message, server_id, created_at, updated_at
|
||||
FROM permanent_blocks
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []PermanentBlockRecord
|
||||
for rows.Next() {
|
||||
var rec PermanentBlockRecord
|
||||
var createdAt, updatedAt sql.NullString
|
||||
if err := rows.Scan(&rec.ID, &rec.IP, &rec.Integration, &rec.Status, &rec.Details, &rec.Message, &rec.ServerID, &createdAt, &updatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if createdAt.Valid {
|
||||
if ts, err := time.Parse(time.RFC3339Nano, createdAt.String); err == nil {
|
||||
rec.CreatedAt = ts
|
||||
}
|
||||
}
|
||||
if updatedAt.Valid {
|
||||
if ts, err := time.Parse(time.RFC3339Nano, updatedAt.String); err == nil {
|
||||
rec.UpdatedAt = ts
|
||||
}
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
// IsPermanentBlockActive returns true when IP is currently blocked by integration.
|
||||
func IsPermanentBlockActive(ctx context.Context, ip, integration string) (bool, error) {
|
||||
rec, found, err := GetPermanentBlock(ctx, ip, integration)
|
||||
if err != nil || !found {
|
||||
return false, err
|
||||
}
|
||||
return rec.Status == "blocked", nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user