Add serverID to all events to sort per fail2ban instance, update language

This commit is contained in:
2025-11-17 20:24:46 +01:00
parent b1bdb66516
commit 2456162b75
10 changed files with 163 additions and 1330 deletions

View File

@@ -22,17 +22,22 @@
"dashboard.cards.total_banned": "Gesamt gesperrte IPs",
"dashboard.cards.new_last_hour": "Neue in der letzten Stunde",
"dashboard.cards.total_logged": "Gespeicherte Sperr-Ereignisse",
"dashboard.cards.recurring_week": "Wiederkehrende IPs (7 Tage)",
"dashboard.cards.recurring_hint": "Behalte wiederholte Angreifer der letzten 7 Tage im Auge.",
"dashboard.table.jail_name": "Jail-Name",
"dashboard.table.total_banned": "Insgesamt gesperrt",
"dashboard.table.new_last_hour": "Neu in letzter Stunde",
"dashboard.table.banned_ips": "Gesperrte IPs (Entsperren)",
"dashboard.no_jails": "Keine Jails gefunden.",
"dashboard.overview_detail": "Listen ein- oder ausklappen, um betroffene Dienste schneller zu sehen.",
"dashboard.table.time": "Zeit",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Logzeile",
"dashboard.no_banned_ips": "Keine gesperrten IPs",
"dashboard.unban": "Entsperren",
"dashboard.banned.show_more": "Mehr anzeigen",
"dashboard.banned.show_less": "Weniger anzeigen",
"logs.overview.title": "Interne Log-Übersicht",
"logs.overview.subtitle": "Von Fail2ban-UI gespeicherte Ereignisse über alle Connectoren.",
"logs.overview.refresh": "Daten aktualisieren",

View File

@@ -22,17 +22,22 @@
"dashboard.cards.total_banned": "Total g'sperrti IPs",
"dashboard.cards.new_last_hour": "Neu i dr letschte Stund",
"dashboard.cards.total_logged": "Gspeichereti Sperr-Ereigniss",
"dashboard.cards.recurring_week": "Widerkehrendi IPs (7 Täg)",
"dashboard.cards.recurring_hint": "Beobachte wiederholti Angreifer us de letschte 7 Täg.",
"dashboard.table.jail_name": "Jail-Name",
"dashboard.table.total_banned": "Insgsamt g'sperrt",
"dashboard.table.new_last_hour": "Neu in dr letschte Stund",
"dashboard.table.banned_ips": "G'sperrti IPs (Entsperre)",
"dashboard.no_jails": "Kei Jails gfunde.",
"dashboard.overview_detail": "Listä zämme- oder usklappe, zum schnäll betroffene Dinst erkenne.",
"dashboard.table.time": "Zyt",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Log-Zile",
"dashboard.no_banned_ips": "Kei g'sperrti IPs",
"dashboard.unban": "Entsperre",
"dashboard.banned.show_more": "Meh azeige",
"dashboard.banned.show_less": "Weniger azeige",
"logs.overview.title": "Interni Log-Übersicht",
"logs.overview.subtitle": "Vo Fail2ban-UI gspeichereti Ereigniss über alli Connectorä.",
"logs.overview.refresh": "Date aktualisiere",

View File

@@ -22,17 +22,22 @@
"dashboard.cards.total_banned": "Total Banned IPs",
"dashboard.cards.new_last_hour": "New Last Hour",
"dashboard.cards.total_logged": "Stored Ban Events",
"dashboard.cards.recurring_week": "Recurring IPs (7 days)",
"dashboard.cards.recurring_hint": "Watch repeated offenders detected in the last seven days.",
"dashboard.table.jail_name": "Jail Name",
"dashboard.table.total_banned": "Total Banned",
"dashboard.table.new_last_hour": "New Last Hour",
"dashboard.table.banned_ips": "Banned IPs (Unban)",
"dashboard.no_jails": "No jails found.",
"dashboard.overview_detail": "Collapse or expand long lists to quickly focus on impacted services.",
"dashboard.table.time": "Time",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Log Line",
"dashboard.no_banned_ips": "No banned IPs",
"dashboard.unban": "Unban",
"dashboard.banned.show_more": "Show more",
"dashboard.banned.show_less": "Hide extra",
"logs.overview.title": "Internal Log Overview",
"logs.overview.subtitle": "Events stored by Fail2ban-UI across all connectors.",
"logs.overview.refresh": "Refresh data",

View File

@@ -22,17 +22,22 @@
"dashboard.cards.total_banned": "IPs bloqueadas totales",
"dashboard.cards.new_last_hour": "Nuevas en la última hora",
"dashboard.cards.total_logged": "Eventos de bloqueo almacenados",
"dashboard.cards.recurring_week": "IPs recurrentes (7 días)",
"dashboard.cards.recurring_hint": "Vigila a los infractores repetidos de los últimos 7 días.",
"dashboard.table.jail_name": "Nombre del Jail",
"dashboard.table.total_banned": "Total bloqueadas",
"dashboard.table.new_last_hour": "Nuevas en la última hora",
"dashboard.table.banned_ips": "IPs bloqueadas (Desbloquear)",
"dashboard.no_jails": "No se encontraron jails.",
"dashboard.overview_detail": "Colapsa o expande las listas largas para centrarte en los servicios afectados.",
"dashboard.table.time": "Hora",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Línea de log",
"dashboard.no_banned_ips": "No hay IP bloqueadas",
"dashboard.unban": "Desbloquear",
"dashboard.banned.show_more": "Mostrar más",
"dashboard.banned.show_less": "Mostrar menos",
"logs.overview.title": "Resumen interno de registros",
"logs.overview.subtitle": "Eventos almacenados por Fail2ban-UI a través de todos los conectores.",
"logs.overview.refresh": "Actualizar datos",

View File

@@ -22,17 +22,22 @@
"dashboard.cards.total_banned": "Total d'IPs bloquées",
"dashboard.cards.new_last_hour": "Nouvelles dans la dernière heure",
"dashboard.cards.total_logged": "Événements de blocage enregistrés",
"dashboard.cards.recurring_week": "IPs récurrentes (7 jours)",
"dashboard.cards.recurring_hint": "Surveillez les récidivistes observés durant les 7 derniers jours.",
"dashboard.table.jail_name": "Nom du Jail",
"dashboard.table.total_banned": "Total bloqués",
"dashboard.table.new_last_hour": "Nouveaux dans la dernière heure",
"dashboard.table.banned_ips": "IPs bloquées (Débloquer)",
"dashboard.no_jails": "Aucun jail trouvé.",
"dashboard.overview_detail": "Réduisez ou développez les longues listes pour vous concentrer sur les services impactés.",
"dashboard.table.time": "Heure",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Ligne de log",
"dashboard.no_banned_ips": "Aucune IP bloquée",
"dashboard.unban": "Débloquer",
"dashboard.banned.show_more": "Afficher plus",
"dashboard.banned.show_less": "Afficher moins",
"logs.overview.title": "Vue d'ensemble interne des journaux",
"logs.overview.subtitle": "Événements enregistrés par Fail2ban-UI sur l'ensemble des connecteurs.",
"logs.overview.refresh": "Actualiser les données",

View File

@@ -22,17 +22,22 @@
"dashboard.cards.total_banned": "IP bloccate totali",
"dashboard.cards.new_last_hour": "Nuove nell'ultima ora",
"dashboard.cards.total_logged": "Eventi di blocco memorizzati",
"dashboard.cards.recurring_week": "IP ricorrenti (7 giorni)",
"dashboard.cards.recurring_hint": "Tieni d'occhio gli indirizzi ricorrenti degli ultimi 7 giorni.",
"dashboard.table.jail_name": "Nome del Jail",
"dashboard.table.total_banned": "Totale bloccate",
"dashboard.table.new_last_hour": "Nuove nell'ultima ora",
"dashboard.table.banned_ips": "IP bloccate (Sblocca)",
"dashboard.no_jails": "Nessun jail trovato.",
"dashboard.overview_detail": "Comprimi o espandi gli elenchi lunghi per concentrarti sui servizi interessati.",
"dashboard.table.time": "Ora",
"dashboard.table.jail": "Jail",
"dashboard.table.ip": "IP",
"dashboard.table.log_line": "Riga di log",
"dashboard.no_banned_ips": "Nessuna IP bloccata",
"dashboard.unban": "Sblocca",
"dashboard.banned.show_more": "Mostra di più",
"dashboard.banned.show_less": "Mostra meno",
"logs.overview.title": "Panoramica interna dei log",
"logs.overview.subtitle": "Eventi memorizzati da Fail2ban-UI su tutti i connettori.",
"logs.overview.refresh": "Aggiorna dati",

View File

@@ -537,8 +537,8 @@ WHERE 1=1`
return result, rows.Err()
}
// CountBanEvents returns total number of ban events optionally filtered by time.
func CountBanEvents(ctx context.Context, since time.Time) (int64, error) {
// CountBanEvents returns total number of ban events optionally filtered by time and server.
func CountBanEvents(ctx context.Context, since time.Time, serverID string) (int64, error) {
if db == nil {
return 0, errors.New("storage not initialised")
}
@@ -549,6 +549,11 @@ FROM ban_events
WHERE 1=1`
args := []any{}
if serverID != "" {
query += " AND server_id = ?"
args = append(args, serverID)
}
if !since.IsZero() {
query += " AND occurred_at >= ?"
args = append(args, since.UTC())
@@ -561,8 +566,8 @@ WHERE 1=1`
return total, nil
}
// CountBanEventsByCountry returns aggregation per country code.
func CountBanEventsByCountry(ctx context.Context, since time.Time) (map[string]int64, error) {
// 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 {
return nil, errors.New("storage not initialised")
}
@@ -573,6 +578,11 @@ FROM ban_events
WHERE 1=1`
args := []any{}
if serverID != "" {
query += " AND server_id = ?"
args = append(args, serverID)
}
if !since.IsZero() {
query += " AND occurred_at >= ?"
args = append(args, since.UTC())
@@ -599,8 +609,8 @@ WHERE 1=1`
return result, rows.Err()
}
// ListRecurringIPStats returns IPs that have been banned at least minCount times.
func ListRecurringIPStats(ctx context.Context, since time.Time, minCount, limit int) ([]RecurringIPStat, error) {
// ListRecurringIPStats returns IPs that have been banned at least minCount times, optionally filtered by server.
func ListRecurringIPStats(ctx context.Context, since time.Time, minCount, limit int, serverID string) ([]RecurringIPStat, error) {
if db == nil {
return nil, errors.New("storage not initialised")
}
@@ -618,6 +628,11 @@ FROM ban_events
WHERE ip != ''`
args := []any{}
if serverID != "" {
query += " AND server_id = ?"
args = append(args, serverID)
}
if !since.IsZero() {
query += " AND occurred_at >= ?"
args = append(args, since.UTC())

View File

@@ -248,6 +248,7 @@ func BanInsightsHandler(c *gin.Context) {
since = parsed
}
}
serverID := c.Query("serverId")
minCount := 3
if minCountStr := c.DefaultQuery("minCount", "3"); minCountStr != "" {
@@ -265,19 +266,19 @@ func BanInsightsHandler(c *gin.Context) {
ctx := c.Request.Context()
countriesMap, err := storage.CountBanEventsByCountry(ctx, since)
countriesMap, err := storage.CountBanEventsByCountry(ctx, since, serverID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
recurring, err := storage.ListRecurringIPStats(ctx, since, minCount, limit)
recurring, err := storage.ListRecurringIPStats(ctx, since, minCount, limit, serverID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
totalOverall, err := storage.CountBanEvents(ctx, time.Time{})
totalOverall, err := storage.CountBanEvents(ctx, time.Time{}, serverID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -285,13 +286,13 @@ func BanInsightsHandler(c *gin.Context) {
now := time.Now().UTC()
totalToday, err := storage.CountBanEvents(ctx, now.Add(-24*time.Hour))
totalToday, err := storage.CountBanEvents(ctx, now.Add(-24*time.Hour), serverID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
totalWeek, err := storage.CountBanEvents(ctx, now.Add(-7*24*time.Hour))
totalWeek, err := storage.CountBanEvents(ctx, now.Add(-7*24*time.Hour), serverID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return

View File

@@ -133,7 +133,7 @@
/* Custom mark styling for search highlights */
mark {
background-color: #fef08a;
padding: 0.1em 0.2em;
padding: 0.1em 0em 0.1em 0.2em;
border-radius: 0.25em;
}
@@ -949,12 +949,31 @@
countries: [],
recurring: []
};
var latestServerInsights = null;
var banEventsFilterText = '';
var banEventsFilterCountry = 'all';
var banEventsFilterDebounce = null;
var translations = {};
var sshKeysCache = null;
function normalizeInsights(data) {
var normalized = data && typeof data === 'object' ? data : {};
if (!normalized.totals || typeof normalized.totals !== 'object') {
normalized.totals = { overall: 0, today: 0, week: 0 };
} else {
normalized.totals.overall = typeof normalized.totals.overall === 'number' ? normalized.totals.overall : 0;
normalized.totals.today = typeof normalized.totals.today === 'number' ? normalized.totals.today : 0;
normalized.totals.week = typeof normalized.totals.week === 'number' ? normalized.totals.week : 0;
}
if (!Array.isArray(normalized.countries)) {
normalized.countries = [];
}
if (!Array.isArray(normalized.recurring)) {
normalized.recurring = [];
}
return normalized;
}
function t(key, fallback) {
if (translations && Object.prototype.hasOwnProperty.call(translations, key) && translations[key]) {
return translations[key];
@@ -1354,20 +1373,37 @@
}
function fetchBanInsightsData() {
return fetch('/api/events/bans/insights')
var sevenDaysAgo = new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString();
var sinceQuery = '?since=' + encodeURIComponent(sevenDaysAgo);
var globalPromise = fetch('/api/events/bans/insights' + sinceQuery)
.then(function(res) { return res.json(); })
.then(function(data) {
latestBanInsights = data || {};
latestBanInsights.totals = latestBanInsights.totals || { overall: 0, today: 0, week: 0 };
latestBanInsights.countries = latestBanInsights.countries || [];
latestBanInsights.recurring = latestBanInsights.recurring || [];
latestBanInsights = normalizeInsights(data);
})
.catch(function(err) {
console.error('Error fetching ban insights:', err);
if (!latestBanInsights) {
latestBanInsights = { totals: { overall: 0, today: 0, week: 0 }, countries: [], recurring: [] };
latestBanInsights = normalizeInsights(null);
}
});
var serverPromise;
if (currentServerId) {
serverPromise = fetch(withServerParam('/api/events/bans/insights' + sinceQuery))
.then(function(res) { return res.json(); })
.then(function(data) {
latestServerInsights = normalizeInsights(data);
})
.catch(function(err) {
console.error('Error fetching server-specific ban insights:', err);
latestServerInsights = null;
});
} else {
latestServerInsights = null;
serverPromise = Promise.resolve();
}
return Promise.all([globalPromise, serverPromise]);
}
function formatDateTime(value) {
@@ -1411,25 +1447,11 @@
}
function recurringIPsLastWeekCount() {
if (!latestBanInsights || !Array.isArray(latestBanInsights.recurring)) {
var source = latestServerInsights || latestBanInsights;
if (!source || !Array.isArray(source.recurring)) {
return 0;
}
var cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000);
var seen = {};
var count = 0;
latestBanInsights.recurring.forEach(function(stat) {
if (!stat || !stat.ip) {
return;
}
var lastSeenTime = stat.lastSeen ? new Date(stat.lastSeen).getTime() : NaN;
if (isNaN(lastSeenTime) || lastSeenTime >= cutoff) {
if (!seen[stat.ip]) {
seen[stat.ip] = true;
count += 1;
}
}
});
return count;
return source.recurring.length;
}
function captureFocusState(container) {
@@ -1460,7 +1482,13 @@
if (!next) {
return;
}
if (typeof next.focus === 'function') {
try {
next.focus({ preventScroll: true });
} catch (err) {
next.focus();
}
}
try {
if (typeof state.selectionStart === 'number' && typeof state.selectionEnd === 'number' && typeof next.setSelectionRange === 'function') {
next.setSelectionRange(state.selectionStart, state.selectionEnd);
@@ -1522,19 +1550,24 @@
});
}
function updateBanEventsSearch(value) {
banEventsFilterText = value || '';
function scheduleLogOverviewRender() {
if (banEventsFilterDebounce) {
clearTimeout(banEventsFilterDebounce);
}
banEventsFilterDebounce = setTimeout(function() {
renderDashboard();
}, 200);
renderLogOverviewSection();
banEventsFilterDebounce = null;
}, 100);
}
function updateBanEventsSearch(value) {
banEventsFilterText = value || '';
scheduleLogOverviewRender();
}
function updateBanEventsCountry(value) {
banEventsFilterCountry = value || 'all';
renderDashboard();
scheduleLogOverviewRender();
}
function getRecurringIPMap() {
@@ -1562,6 +1595,7 @@
+ ' <p class="text-sm mt-1" data-i18n="dashboard.no_servers_body">Add a server to start monitoring and controlling Fail2ban instances.</p>'
+ '</div>';
if (typeof updateTranslations === 'function') updateTranslations();
restoreFocusState(focusState);
return;
}
if (!enabledServers.length) {
@@ -1571,6 +1605,7 @@
+ ' <p class="text-sm mt-1" data-i18n="dashboard.no_enabled_servers_body">Enable the local connector or register a remote Fail2ban server to see live data.</p>'
+ '</div>';
if (typeof updateTranslations === 'function') updateTranslations();
restoreFocusState(focusState);
return;
}
@@ -1667,7 +1702,7 @@
html += '</div>'; // close overview card
}
html += renderLogOverview();
html += '<div id="logOverview">' + renderLogOverviewContent() + '</div>';
container.innerHTML = html;
restoreFocusState(focusState);
@@ -1693,7 +1728,18 @@
}
}
function renderLogOverview() {
function renderLogOverviewSection() {
var target = document.getElementById('logOverview');
if (!target) return;
var focusState = captureFocusState(target);
target.innerHTML = renderLogOverviewContent();
restoreFocusState(focusState);
if (typeof updateTranslations === 'function') {
updateTranslations();
}
}
function renderLogOverviewContent() {
var html = ''
+ '<div class="bg-white rounded-lg shadow p-6 mb-6">'
+ ' <div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between mb-4">'
@@ -1770,6 +1816,7 @@
var countries = getBanEventCountries();
var filteredEvents = getFilteredBanEvents();
var recurringMap = getRecurringIPMap();
var searchQuery = (banEventsFilterText || '').trim();
html += ''
+ '<div class="flex flex-col sm:flex-row gap-3 mb-4">'
@@ -1817,15 +1864,20 @@
var index = latestBanEvents.indexOf(event);
var hasWhois = event.whois && event.whois.trim().length > 0;
var hasLogs = event.logs && event.logs.trim().length > 0;
var ipCell = escapeHtml(event.ip || '');
var serverValue = event.serverName || event.serverId || '';
var jailValue = event.jail || '';
var ipValue = event.ip || '';
var serverCell = highlightQueryMatch(serverValue, searchQuery);
var jailCell = highlightQueryMatch(jailValue, searchQuery);
var ipCell = highlightQueryMatch(ipValue, searchQuery);
if (event.ip && recurringMap[event.ip]) {
ipCell += ' <span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">' + t('logs.badge.recurring', 'Recurring') + '</span>';
}
html += ''
+ ' <tr class="hover:bg-gray-50">'
+ ' <td class="px-2 py-2 whitespace-nowrap">' + escapeHtml(formatDateTime(event.occurredAt || event.createdAt)) + '</td>'
+ ' <td class="px-2 py-2 whitespace-nowrap">' + escapeHtml(event.serverName || event.serverId || '') + '</td>'
+ ' <td class="hidden sm:table-cell px-2 py-2 whitespace-nowrap">' + escapeHtml(event.jail || '') + '</td>'
+ ' <td class="px-2 py-2 whitespace-nowrap">' + serverCell + '</td>'
+ ' <td class="hidden sm:table-cell px-2 py-2 whitespace-nowrap">' + jailCell + '</td>'
+ ' <td class="px-2 py-2 whitespace-nowrap">' + ipCell + '</td>'
+ ' <td class="hidden md:table-cell px-2 py-2 whitespace-nowrap">' + escapeHtml(event.country || '—') + '</td>'
+ ' <td class="px-2 py-2 whitespace-nowrap">'
@@ -2251,6 +2303,24 @@
});
}
function highlightQueryMatch(value, query) {
var text = value || '';
if (!query) {
return escapeHtml(text);
}
var escapedPattern = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (!escapedPattern) {
return escapeHtml(text);
}
var regex = new RegExp(escapedPattern, "gi");
var highlighted = text.replace(regex, function(match) {
return "%%MARK_START%%" + match + "%%MARK_END%%";
});
return escapeHtml(highlighted)
.replace(/%%MARK_START%%/g, "<mark>")
.replace(/%%MARK_END%%/g, "</mark>");
}
// Render banned IPs with "Unban" button
function slugifyId(value, prefix) {
var input = (value || '').toString();
@@ -2377,15 +2447,7 @@
} else if (originalIP.indexOf(query) !== -1) {
// If the IP contains the query, show the item and highlight the matching text.
item.style.display = "";
const escapedPattern = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(escapedPattern, "gi");
var highlighted = originalIP.replace(regex, function(match) {
return "%%MARK_START%%" + match + "%%MARK_END%%";
});
var safeHighlighted = escapeHtml(highlighted)
.replace(/%%MARK_START%%/g, "<mark>")
.replace(/%%MARK_END%%/g, "</mark>");
span.innerHTML = safeHighlighted;
span.innerHTML = highlightQueryMatch(originalIP, query);
rowHasMatch = true;
} else {
item.style.display = "none";

File diff suppressed because it is too large Load Diff