diff --git a/internal/locales/de.json b/internal/locales/de.json index 93c89c7..192749d 100644 --- a/internal/locales/de.json +++ b/internal/locales/de.json @@ -29,7 +29,7 @@ "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.overview_detail": "Die Listen üssen nicht ausgeklappt werden, um eine IP zu suchen.", "dashboard.table.time": "Zeit", "dashboard.table.jail": "Jail", "dashboard.table.ip": "IP", @@ -76,6 +76,11 @@ "logs.modal.insights_description": "Verteilung nach Ländern und wiederholte Angreifer.", "logs.modal.insights_countries": "Sperren nach Land", "logs.modal.insights_countries_empty": "Für diesen Zeitraum wurden keine Sperren erfasst.", + "logs.modal.insights_countries_hint": "Top-Herkünfte im ausgewählten Zeitraum.", + "logs.modal.insights_recurring_hint": "IP-Adressen, die Fail2ban wiederholt auslösen.", + "logs.modal.total_overall_note": "Lebenslang erfasste Sperren", + "logs.modal.total_today_note": "Letzte 24 Stunden", + "logs.modal.total_week_note": "Aktivität der letzten Woche", "logs.modal.insights_recurring": "Wiederkehrende IPs", "logs.modal.insights_recurring_empty": "Keine wiederkehrenden IPs erkannt.", "filter_debug.title": "Filter-Debug", @@ -221,6 +226,29 @@ "servers.form.select_key_placeholder": "Manuelle Eingabe", "servers.form.no_keys": "Keine SSH-Schlüssel gefunden; Pfad manuell eingeben", "filter_debug.not_available": "Filter-Debug ist nur für lokale Connectoren verfügbar.", - "filter_debug.local_missing": "Das lokale Fail2ban-Filterverzeichnis wurde auf diesem Host nicht gefunden." + "filter_debug.local_missing": "Das lokale Fail2ban-Filterverzeichnis wurde auf diesem Host nicht gefunden.", + "email.ban.title": "Achtung: Fail2Ban hat eine neue IP-Adresse blockiert", + "email.ban.intro": "Fail2Ban-UI hat eine fehlerhafte Anfrage oder wiederholte Authentifizierungsfehler erkannt und die Quell-IP automatisch blockiert. Überprüfen Sie die Metadaten und Log-Auszüge unten.", + "email.ban.subject.banned": "Gesperrt", + "email.ban.subject.from": "von", + "email.ban.details.banned_ip": "Gesperrte IP", + "email.ban.details.jail": "Jail", + "email.ban.details.hostname": "Hostname", + "email.ban.details.failed_attempts": "Fehlgeschlagene Versuche", + "email.ban.details.country": "Land", + "email.ban.details.timestamp": "Zeitstempel", + "email.ban.whois_title": "WHOIS-Auszug", + "email.ban.logs_title": "Relevante Logs", + "email.test.title": "E-Mail-Zustellungstest", + "email.test.intro": "Diese Nachricht bestätigt, dass Ihre SMTP-Konfiguration korrekt funktioniert und HTML-formatierte E-Mails zustellen kann.", + "email.test.subject": "Test-E-Mail von Fail2Ban UI", + "email.test.details.recipient": "Empfänger", + "email.test.details.smtp_host": "SMTP-Host", + "email.test.details.triggered_at": "Ausgelöst um", + "email.test.whois_no_data": "Für Test-E-Mails wird keine WHOIS-Abfrage durchgeführt.", + "email.test.sample_logs": "2025-01-01T12:00:00Z Beispiel-Log-Eintrag von Fail2ban-UI.", + "email.whois.no_data": "WHOIS-Daten wurden für dieses Ereignis nicht erfasst.", + "email.logs.no_data": "Für diesen Block wurden keine Log-Einträge erfasst.", + "email.footer.text": "Diese Nachricht wurde automatisch von Fail2Ban-UI generiert" } \ No newline at end of file diff --git a/internal/locales/de_ch.json b/internal/locales/de_ch.json index 0a99abb..b557cc8 100644 --- a/internal/locales/de_ch.json +++ b/internal/locales/de_ch.json @@ -3,53 +3,53 @@ "nav.dashboard": "Dashboard", "nav.filter_debug": "Filter Debug", "nav.settings": "Istellige", - "restart_banner.message": "Fail2ban Konfiguration gänderet! Für d'Änderige z'überneh, bitte: ", + "restart_banner.message": "Fail2ban Konfiguration gänderet! Für z'übernehä, bitte: ", "restart_banner.button": "Service neu starte", "dashboard.title": "Dashboard", "dashboard.overview": "Übersicht vo de aktive Jails und Blocks", - "dashboard.overview_hint": "Bruch d Suechi zum g'sperrti IPs filtere und klick uf es Jail, zum d Konfiguration z'bearbeite.", - "dashboard.search_label": "Suech nach g'sperrte IPs", - "dashboard.search_placeholder": "Gib d'IP adrässe i, wo du suechsch", + "dashboard.overview_hint": "Bruch d Suechi zum g'sperrti IPs filtere und klick ufneä Jail, zum Filter bearbeite.", + "dashboard.search_label": "Suech nachere g'sperrte IP", + "dashboard.search_placeholder": "Gib d'IP i, wo suechsch", "dashboard.external_ip": "Dini ext. IP:", "dashboard.manage_servers": "Server verwalte", - "dashboard.no_servers_title": "Kei Fail2ban-Server konfiguriert", - "dashboard.no_servers_body": "Füeg en Server dezue zum Fail2ban-Instanze überwache und steuere.", - "dashboard.loading_summary": "Lad Zämmefassig…", - "dashboard.no_enabled_servers_title": "Kei aktivi Verbindige", - "dashboard.no_enabled_servers_body": "Aktivier dr lokale Connector oder registrier ä entfernten Fail2ban-Server für Live-Datä.", + "dashboard.no_servers_title": "Ke Fail2ban-Server konfiguriert", + "dashboard.no_servers_body": "Füeg ä witere Server hinzue um ou die Fail2ban-Instanz z'überwache und z'steuerä.", + "dashboard.loading_summary": "Laded Zämmefassig…", + "dashboard.no_enabled_servers_title": "Ke aktivi Verbindige", + "dashboard.no_enabled_servers_body": "Aktivier dr lokal Connector oder registrier ä entfernte Fail2ban-Server.", "dashboard.errors.summary_failed": "Zämmefassig het nid chönne glade wärde.", "dashboard.cards.active_jails": "Aktivi Jails", "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.total_logged": "Gspichereti 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.cards.recurring_hint": "Wiederholendi Täterschaft 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.table.new_last_hour": "Neu ir letschte Stund", + "dashboard.table.banned_ips": "G'sperrti IPs (Entsperrä)", "dashboard.no_jails": "Kei Jails gfunde.", - "dashboard.overview_detail": "Listä zämme- oder usklappe, zum schnäll betroffene Dinst erkenne.", + "dashboard.overview_detail": "Aui Listä usklappe isch um IPs z suche nid nötig.", "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.no_banned_ips": "Ke 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.title": "Generelli Log-Übersicht", + "logs.overview.subtitle": "Vom Fail2ban-UI gspeichereti Ereigniss über alli Connectorä.", "logs.overview.refresh": "Date aktualisiere", "logs.overview.total_events": "Total gspeichereti Ereigniss", "logs.overview.per_server": "Ereigniss pro Server", "logs.overview.recent_events_title": "Letschti gspeichereti Ereigniss", - "logs.overview.recent_empty": "Kei gspeichereti Ereigniss gfunde.", - "logs.overview.empty": "No kei Sperr-Ereigniss protokolliert.", + "logs.overview.recent_empty": "No keni gspeichereti Ereigniss gfunde.", + "logs.overview.empty": "No keni Sperr-Ereigniss protokolliert.", "logs.overview.open_insights": "Insights azeige", "logs.overview.total_today": "Hüt", "logs.overview.total_week": "Letschti 7 Täg", - "logs.overview.per_server_empty": "No kei Serverdate verfügbar.", + "logs.overview.per_server_empty": "No keni Serverdate verfügbar.", "logs.overview.recent_filtered_empty": "Kei Ereigniss erfülle d Filter.", "logs.overview.recent_count_label": "Aazeigti Ereigniss", "logs.overview.country_unknown": "Unbekannt", @@ -75,16 +75,21 @@ "logs.modal.insights_title": "Ban-Insights", "logs.modal.insights_description": "Verteilig nach Länder und wiederholti Angreifer.", "logs.modal.insights_countries": "Sperre nach Land", - "logs.modal.insights_countries_empty": "Kei Sperre i däm Zeitraum.", + "logs.modal.insights_countries_empty": "Keni Sperrig i däm Zeitraum.", + "logs.modal.insights_countries_hint": "Hauptherkunft im gwählte Zeitraum.", + "logs.modal.insights_recurring_hint": "IP-Adrässe, wo Fail2ban mehfach uslöse.", + "logs.modal.total_overall_note": "Gsamti Sperre bis jetzt", + "logs.modal.total_today_note": "Letschti 24 Stund", + "logs.modal.total_week_note": "Aktivität vor letschte Wuche", "logs.modal.insights_recurring": "Wiederkehrendi IPs", - "logs.modal.insights_recurring_empty": "Kei wiederkehrendi IPs erkannt.", + "logs.modal.insights_recurring_empty": "Ke wiederkehrendi IPs erkannt.", "filter_debug.title": "Filter Debug", - "filter_debug.select_filter": "Wähl en Filter us", + "filter_debug.select_filter": "Wähl ä Filter us", "filter_debug.log_lines": "Log-Zile", - "filter_debug.log_lines_placeholder": "Gib ä Log-Zile da ii...", + "filter_debug.log_lines_placeholder": "Füeg ä Log-Zile ii...", "filter_debug.test_filter": "Filter teste", "filter_debug.test_results_title": "Testergebnis", - "filter_debug.no_matches": "Kei Übereinstimmige gfunde.", + "filter_debug.no_matches": "Ke Übereinstimmige gfunde.", "settings.title": "Istellige", "settings.general": "Allgemeini Istellige", "settings.language": "Sprach", @@ -92,10 +97,10 @@ "settings.alert": "Alarm-Istellige", "settings.callback_url": "Fail2ban Callback-URL", "settings.callback_url_placeholder": "http://127.0.0.1:8080", - "settings.destination_email": "Ziil-Email (Alarmempfänger)", + "settings.destination_email": "Ziiu-Email (Alarmempfänger)", "settings.destination_email_placeholder": "alerts@swissmakers.ch", "settings.alert_countries": "Alarm-Länder", - "settings.alert_countries_description": "Wähl d'Länder us, für weli du per Email ä Alarm becho wetsch, wenn e Sperrig passiert.", + "settings.alert_countries_description": "Wähl d'Länder us, für weli du per Email ä Alarm becho wetsch, wenn e Sperrig erfolgt.", "settings.smtp": "SMTP-Konfiguration", "settings.smtp_host": "SMTP-Host", "settings.smtp_host_placeholder": "z.B. smtp.gmail.com", @@ -119,30 +124,30 @@ "settings.ignore_ips": "IPs ignorierä", "settings.ignore_ips_placeholder": "IPs, getrennt dur e Leerzeichä", "settings.advanced.title": "Erwieterti Aktione für Wiederholungstäter", - "settings.advanced.description": "Synchronisiere wiederholti Täters automatisch mit ere externe Firewall oder Sperrlischt.", + "settings.advanced.description": "Synchronisier d wiederholigstäter automatisch mit dr externe Firewall resp. Sperrlischte.", "settings.advanced.refresh_log": "Log aktualisiere", "settings.advanced.test_button": "Integration teste", - "settings.advanced.enable": "Automatischi permanente Sperri aktiviere", - "settings.advanced.threshold": "Schwelle vor de permanente Sperri", - "settings.advanced.threshold_hint": "Sobald e IP die Zah erreitcht, wird sie a d Integration übergeh.", + "settings.advanced.enable": "Automatischi permanenti Sperrig aktiviere", + "settings.advanced.threshold": "Schweue für die permanenti Sperrig", + "settings.advanced.threshold_hint": "Sobald e IP die Zahu erreitcht, wird sie via Integration gsperrt.", "settings.advanced.integration": "Integration", "settings.advanced.integration_none": "Integration uswähle", - "settings.advanced.integration_hint": "Wähl d Firewall oder Appliance, wo d permanente Sperre sött ahlegt werde.", - "settings.advanced.mikrotik.note": "Git d'SSH-Zuegriff uf din Mikrotik-Router a und d Address-Lischt, wo d'Sperre ine chöme.", + "settings.advanced.integration_hint": "Wähl d Firewall oder Appliance, wo diä permanenti Sperreg sött dürefüehre.", + "settings.advanced.mikrotik.note": "Gib dr SSH-Zuegriff uf di Mikrotik-Router a und d Address-Lischte, wo d'Sperrige ihtreit wärde.", "settings.advanced.mikrotik.host": "Host", "settings.advanced.mikrotik.port": "Port", "settings.advanced.mikrotik.username": "SSH-Benutzername", "settings.advanced.mikrotik.password": "SSH-Passwort", "settings.advanced.mikrotik.key": "SSH-Key-Pfad (optional)", - "settings.advanced.mikrotik.list": "Adress-Lischtname", - "settings.advanced.pfsense.note": "Bruucht s pfSense API-Päckli. Nimm es Token wo Aliase cha bearbeite.", + "settings.advanced.mikrotik.list": "Adress-Lischtename", + "settings.advanced.pfsense.note": "Bruucht s pfSense API-Päckli. Erstell es Token wo Aliase cha bearbeite.", "settings.advanced.pfsense.base_url": "Basis-URL", "settings.advanced.pfsense.token": "API-Token", "settings.advanced.pfsense.secret": "API-Secret", "settings.advanced.pfsense.alias": "Alias-Name", "settings.advanced.pfsense.skip_tls": "TLS-Prüfig überspringe (Self-Signed)", - "settings.advanced.log_title": "Log vo de permanente Sperre", - "settings.advanced.log_empty": "No kei permanente Sperre erfasst.", + "settings.advanced.log_title": "Permanent gsperrti IPs", + "settings.advanced.log_empty": "No ke permanenti Sperrig erfasst.", "settings.advanced.log_ip": "IP", "settings.advanced.log_integration": "Integration", "settings.advanced.log_status": "Status", @@ -153,10 +158,10 @@ "settings.advanced.unblock_btn": "Entferne", "settings.advanced.test_title": "Integration teste", "settings.advanced.test_ip": "IP-Adrässe", - "settings.advanced.test_server": "Optionaler Server", + "settings.advanced.test_server": "Optionale Server", "settings.advanced.test_server_none": "Globali Integration bruuchä", "settings.advanced.test_block": "IP sperre", - "settings.advanced.test_unblock": "IP entferne", + "settings.advanced.test_unblock": "IP freigäh", "settings.save": "Speicherä", "modal.filter_config": "Filter-Konfiguration:", "modal.filter_config_edit": "Filter bearbeite", @@ -221,6 +226,29 @@ "servers.form.select_key_placeholder": "Manuäll igäh", "servers.form.no_keys": "Kei SSH-Schlüssel gfunde; Pfad selber igäh", "filter_debug.not_available": "Filter-Debug git's nur für lokal Connectorä.", - "filter_debug.local_missing": "S lokale Fail2ban-Filterverzeichnis isch uf däm Host nid gfunde worde." + "filter_debug.local_missing": "S lokale Fail2ban-Filterverzeichnis isch uf däm Host nid gfunde worde.", + "email.ban.title": "Achtung: Fail2Ban het e nöi IP-Adrässe blockiert", + "email.ban.intro": "Fail2Ban-UI het e fehlerhafti Aafrag oder widerholti Authentifizierigsfähler erkennt und d Quell-IP automatisch blockiert. Überprüef d Metadate und Log-Uuszüg unge.", + "email.ban.subject.banned": "G'sperrt", + "email.ban.subject.from": "vo", + "email.ban.details.banned_ip": "G'sperrti IP", + "email.ban.details.jail": "Jail", + "email.ban.details.hostname": "Hostname", + "email.ban.details.failed_attempts": "Fehlgschlageni Versüech", + "email.ban.details.country": "Land", + "email.ban.details.timestamp": "Zytstämpfel", + "email.ban.whois_title": "WHOIS-Uszuug", + "email.ban.logs_title": "Relevanti Logs", + "email.test.title": "E-Mail-Zustelligstest", + "email.test.intro": "Diä Nachricht bestätigt, dass dini SMTP-Konfiguration korrekt funktioniert und HTML-formatierti E-Mails zuegstellt chöi wärde.", + "email.test.subject": "Test-E-Mail vom Fail2Ban UI", + "email.test.details.recipient": "Empfänger", + "email.test.details.smtp_host": "SMTP-Host", + "email.test.details.triggered_at": "Usglöst um", + "email.test.whois_no_data": "Für Test-E-Mails wird keni WHOIS-Abfrag düregfühert.", + "email.test.sample_logs": "2025-01-01T12:00:00Z Bispil-Log-Itrag vom Fail2ban-UI.", + "email.whois.no_data": "WHOIS-Date si für das Ereignis nid erfasst worde.", + "email.logs.no_data": "Für de Block sind keni Log-Iiträg erfasst worde.", + "email.footer.text": "Diä Nachricht isch automatisch vom Fail2Ban-UI generiert worde" } \ No newline at end of file diff --git a/internal/locales/en.json b/internal/locales/en.json index db16bf0..3668c9f 100644 --- a/internal/locales/en.json +++ b/internal/locales/en.json @@ -29,7 +29,7 @@ "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.overview_detail": "The lists must not expanded to search for an IP.", "dashboard.table.time": "Time", "dashboard.table.jail": "Jail", "dashboard.table.ip": "IP", @@ -76,6 +76,11 @@ "logs.modal.insights_description": "Country distribution and recurring offenders.", "logs.modal.insights_countries": "Bans by country", "logs.modal.insights_countries_empty": "No bans recorded for this period.", + "logs.modal.insights_countries_hint": "Top origins for the selected time range.", + "logs.modal.insights_recurring_hint": "IP addresses repeatedly triggering Fail2ban.", + "logs.modal.total_overall_note": "Lifetime bans recorded", + "logs.modal.total_today_note": "Last 24 hours", + "logs.modal.total_week_note": "Weekly activity", "logs.modal.insights_recurring": "Recurring IPs", "logs.modal.insights_recurring_empty": "No recurring IPs detected.", "filter_debug.title": "Filter Debug", @@ -221,6 +226,29 @@ "servers.form.select_key_placeholder": "Manual entry", "servers.form.no_keys": "No SSH keys found; enter path manually", "filter_debug.not_available": "Filter debug is only available for local connectors.", - "filter_debug.local_missing": "The local Fail2ban filter directory was not found on this host." + "filter_debug.local_missing": "The local Fail2ban filter directory was not found on this host.", + "email.ban.title": "Security alert: Fail2Ban blocked a new IP-address", + "email.ban.intro": "Fail2Ban-UI detected a bad request or repeated authentication failures and automatically blocked the source-IP. Review the metadata and log excerpts below.", + "email.ban.subject.banned": "Banned", + "email.ban.subject.from": "from", + "email.ban.details.banned_ip": "Banned IP", + "email.ban.details.jail": "Jail", + "email.ban.details.hostname": "Hostname", + "email.ban.details.failed_attempts": "Failed attempts", + "email.ban.details.country": "Country", + "email.ban.details.timestamp": "Timestamp", + "email.ban.whois_title": "WHOIS footprint", + "email.ban.logs_title": "Relevant log excerpts", + "email.test.title": "Email delivery test", + "email.test.intro": "This message confirms that your SMTP configuration is working correctly and can deliver HTML formatted emails.", + "email.test.subject": "Test Email from Fail2Ban UI", + "email.test.details.recipient": "Recipient", + "email.test.details.smtp_host": "SMTP host", + "email.test.details.triggered_at": "Triggered at", + "email.test.whois_no_data": "No WHOIS lookup is executed for test emails.", + "email.test.sample_logs": "2025-01-01T12:00:00Z Sample log entry of Fail2ban-UI.", + "email.whois.no_data": "WHOIS data was not captured for this event.", + "email.logs.no_data": "No log entries were captured for this block.", + "email.footer.text": "This message was generated automatically by Fail2Ban-UI" } \ No newline at end of file diff --git a/internal/locales/es.json b/internal/locales/es.json index 90f7fce..f692a3b 100644 --- a/internal/locales/es.json +++ b/internal/locales/es.json @@ -76,6 +76,11 @@ "logs.modal.insights_description": "Distribución por país y atacantes recurrentes.", "logs.modal.insights_countries": "Bloqueos por país", "logs.modal.insights_countries_empty": "No se registraron bloqueos en este periodo.", + "logs.modal.insights_countries_hint": "Principales orígenes en el período seleccionado.", + "logs.modal.insights_recurring_hint": "IPs que activan Fail2ban de forma recurrente.", + "logs.modal.total_overall_note": "Bloqueos acumulados", + "logs.modal.total_today_note": "Últimas 24 horas", + "logs.modal.total_week_note": "Actividad semanal", "logs.modal.insights_recurring": "IPs recurrentes", "logs.modal.insights_recurring_empty": "No se detectaron IPs recurrentes.", "filter_debug.title": "Depuración de filtros", @@ -220,6 +225,29 @@ "servers.form.select_key": "Seleccionar clave privada", "servers.form.select_key_placeholder": "Entrada manual", "servers.form.no_keys": "No se encontraron claves SSH; introduzca la ruta manualmente", - "filter_debug.not_available": "La depuración de filtros solo está disponible para conectores locales.", - "filter_debug.local_missing": "No se encontró el directorio de filtros local de Fail2ban en este host." -} + "filter_debug.not_available": "La depuración de filtros solo está disponible para conectores locales.", + "filter_debug.local_missing": "No se encontró el directorio de filtros local de Fail2ban en este host.", + "email.ban.title": "Alerta de seguridad: Fail2Ban bloqueó una nueva dirección IP", + "email.ban.intro": "Fail2Ban-UI detectó una solicitud incorrecta o fallos de autenticación repetidos y bloqueó automáticamente la IP de origen. Revise los metadatos y extractos de registro a continuación.", + "email.ban.subject.banned": "Bloqueado", + "email.ban.subject.from": "desde", + "email.ban.details.banned_ip": "IP bloqueada", + "email.ban.details.jail": "Jail", + "email.ban.details.hostname": "Nombre de host", + "email.ban.details.failed_attempts": "Intentos fallidos", + "email.ban.details.country": "País", + "email.ban.details.timestamp": "Marca de tiempo", + "email.ban.whois_title": "Huella WHOIS", + "email.ban.logs_title": "Extractos de registro relevantes", + "email.test.title": "Prueba de entrega de correo electrónico", + "email.test.intro": "Este mensaje confirma que su configuración SMTP funciona correctamente y puede entregar correos electrónicos con formato HTML.", + "email.test.subject": "Correo de prueba de Fail2Ban UI", + "email.test.details.recipient": "Destinatario", + "email.test.details.smtp_host": "Host SMTP", + "email.test.details.triggered_at": "Activado en", + "email.test.whois_no_data": "No se ejecuta búsqueda WHOIS para correos de prueba.", + "email.test.sample_logs": "2025-01-01T12:00:00Z Entrada de registro de ejemplo de Fail2ban-UI.", + "email.whois.no_data": "No se capturaron datos WHOIS para este evento.", + "email.logs.no_data": "No se capturaron entradas de registro para este bloqueo.", + "email.footer.text": "Este mensaje fue generado automáticamente por Fail2Ban-UI" + } diff --git a/internal/locales/fr.json b/internal/locales/fr.json index df6b0ab..4db54f1 100644 --- a/internal/locales/fr.json +++ b/internal/locales/fr.json @@ -76,6 +76,11 @@ "logs.modal.insights_description": "Répartition par pays et IP récurrentes.", "logs.modal.insights_countries": "Blocages par pays", "logs.modal.insights_countries_empty": "Aucun blocage enregistré pour cette période.", + "logs.modal.insights_countries_hint": "Principales origines pour la période sélectionnée.", + "logs.modal.insights_recurring_hint": "Adresses IP déclenchant Fail2ban à répétition.", + "logs.modal.total_overall_note": "Blocages enregistrés depuis l'origine", + "logs.modal.total_today_note": "Dernières 24 heures", + "logs.modal.total_week_note": "Activité hebdomadaire", "logs.modal.insights_recurring": "IPs récurrentes", "logs.modal.insights_recurring_empty": "Aucune IP récurrente détectée.", "filter_debug.title": "Débogage des filtres", @@ -220,6 +225,29 @@ "servers.form.select_key": "Sélectionner la clé privée", "servers.form.select_key_placeholder": "Saisie manuelle", "servers.form.no_keys": "Aucune clé SSH trouvée ; saisissez le chemin manuellement", - "filter_debug.not_available": "Le débogage des filtres n'est disponible que pour les connecteurs locaux.", - "filter_debug.local_missing": "Le répertoire de filtres Fail2ban local est introuvable sur cet hôte." -} + "filter_debug.not_available": "Le débogage des filtres n'est disponible que pour les connecteurs locaux.", + "filter_debug.local_missing": "Le répertoire de filtres Fail2ban local est introuvable sur cet hôte.", + "email.ban.title": "Alerte de sécurité : Fail2Ban a bloqué une nouvelle adresse IP", + "email.ban.intro": "Fail2Ban-UI a détecté une requête suspecte ou des échecs d'authentification répétés et a automatiquement bloqué l'IP source. Consultez les métadonnées et extraits de journaux ci-dessous.", + "email.ban.subject.banned": "Bloqué", + "email.ban.subject.from": "depuis", + "email.ban.details.banned_ip": "IP bloquée", + "email.ban.details.jail": "Jail", + "email.ban.details.hostname": "Nom d'hôte", + "email.ban.details.failed_attempts": "Tentatives échouées", + "email.ban.details.country": "Pays", + "email.ban.details.timestamp": "Horodatage", + "email.ban.whois_title": "Empreinte WHOIS", + "email.ban.logs_title": "Extraits de journaux pertinents", + "email.test.title": "Test de livraison d'email", + "email.test.intro": "Ce message confirme que votre configuration SMTP fonctionne correctement et peut livrer des emails formatés en HTML.", + "email.test.subject": "Email de test de Fail2Ban UI", + "email.test.details.recipient": "Destinataire", + "email.test.details.smtp_host": "Hôte SMTP", + "email.test.details.triggered_at": "Déclenché à", + "email.test.whois_no_data": "Aucune recherche WHOIS n'est exécutée pour les emails de test.", + "email.test.sample_logs": "2025-01-01T12:00:00Z Entrée de journal d'exemple de Fail2ban-UI.", + "email.whois.no_data": "Les données WHOIS n'ont pas été capturées pour cet événement.", + "email.logs.no_data": "Aucune entrée de journal n'a été capturée pour ce blocage.", + "email.footer.text": "Ce message a été généré automatiquement par Fail2Ban-UI" + } diff --git a/internal/locales/it.json b/internal/locales/it.json index abfa9fa..764d82d 100644 --- a/internal/locales/it.json +++ b/internal/locales/it.json @@ -76,6 +76,11 @@ "logs.modal.insights_description": "Distribuzione per paese e IP ricorrenti.", "logs.modal.insights_countries": "Blocchi per paese", "logs.modal.insights_countries_empty": "Nessun blocco registrato per questo periodo.", + "logs.modal.insights_countries_hint": "Origini principali per l'intervallo selezionato.", + "logs.modal.insights_recurring_hint": "IP che attivano ripetutamente Fail2ban.", + "logs.modal.total_overall_note": "Blocchi totali registrati", + "logs.modal.total_today_note": "Ultime 24 ore", + "logs.modal.total_week_note": "Attività settimanale", "logs.modal.insights_recurring": "IP ricorrenti", "logs.modal.insights_recurring_empty": "Nessun IP ricorrente rilevato.", "filter_debug.title": "Debug Filtro", @@ -220,6 +225,29 @@ "servers.form.select_key": "Seleziona chiave privata", "servers.form.select_key_placeholder": "Inserimento manuale", "servers.form.no_keys": "Nessuna chiave SSH trovata; inserire il percorso manualmente", - "filter_debug.not_available": "Il debug dei filtri è disponibile solo per i connettori locali.", - "filter_debug.local_missing": "La directory dei filtri Fail2ban locale non è stata trovata su questo host." -} + "filter_debug.not_available": "Il debug dei filtri è disponibile solo per i connettori locali.", + "filter_debug.local_missing": "La directory dei filtri Fail2ban locale non è stata trovata su questo host.", + "email.ban.title": "Allerta di sicurezza: Fail2Ban ha bloccato un nuovo indirizzo IP", + "email.ban.intro": "Fail2Ban-UI ha rilevato una richiesta sospetta o ripetuti fallimenti di autenticazione e ha automaticamente bloccato l'IP sorgente. Rivedere i metadati e gli estratti di log di seguito.", + "email.ban.subject.banned": "Bloccato", + "email.ban.subject.from": "da", + "email.ban.details.banned_ip": "IP bloccato", + "email.ban.details.jail": "Jail", + "email.ban.details.hostname": "Nome host", + "email.ban.details.failed_attempts": "Tentativi falliti", + "email.ban.details.country": "Paese", + "email.ban.details.timestamp": "Timestamp", + "email.ban.whois_title": "Impronta WHOIS", + "email.ban.logs_title": "Estratti di log rilevanti", + "email.test.title": "Test di consegna email", + "email.test.intro": "Questo messaggio conferma che la configurazione SMTP funziona correttamente e può consegnare email formattate in HTML.", + "email.test.subject": "Email di test da Fail2Ban UI", + "email.test.details.recipient": "Destinatario", + "email.test.details.smtp_host": "Host SMTP", + "email.test.details.triggered_at": "Attivato alle", + "email.test.whois_no_data": "Nessuna ricerca WHOIS viene eseguita per le email di test.", + "email.test.sample_logs": "2025-01-01T12:00:00Z Voce di log di esempio di Fail2ban-UI.", + "email.whois.no_data": "I dati WHOIS non sono stati acquisiti per questo evento.", + "email.logs.no_data": "Nessuna voce di log è stata acquisita per questo blocco.", + "email.footer.text": "Questo messaggio è stato generato automaticamente da Fail2Ban-UI" + } diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index 0d2ea06..bda79e5 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -20,8 +20,10 @@ import ( "bytes" "context" "crypto/tls" + "encoding/json" "errors" "fmt" + "html" "io" "log" "net" @@ -29,9 +31,11 @@ import ( "net/smtp" "os" "path/filepath" + "regexp" "sort" "strconv" "strings" + "sync" "time" "github.com/gin-gonic/gin" @@ -47,6 +51,32 @@ type SummaryResponse struct { Jails []fail2ban.JailInfo `json:"jails"` } +type emailDetail struct { + Label string + Value string +} + +var ( + httpQuotedStatusPattern = regexp.MustCompile(`"[^"]*"\s+(\d{3})\b`) + httpPlainStatusPattern = regexp.MustCompile(`\s(\d{3})\s+(?:\d+|-)`) + suspiciousLogIndicators = []string{ + "select ", + "union ", + "/etc/passwd", + "/xmlrpc.php", + "/wp-admin", + "/cgi-bin", + "cmd=", + "wget", + "curl ", + "nslookup", + "content-length: 0", + "${", + } + localeCache = make(map[string]map[string]string) + localeCacheLock sync.RWMutex +) + func resolveConnector(c *gin.Context) (fail2ban.Connector, error) { serverID := c.Query("serverId") if serverID == "" { @@ -960,6 +990,88 @@ func RestartFail2banHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Fail2ban restarted successfully"}) } +// loadLocale loads a locale JSON file and returns a map of translations +func loadLocale(lang string) (map[string]string, error) { + localeCacheLock.RLock() + if cached, ok := localeCache[lang]; ok { + localeCacheLock.RUnlock() + return cached, nil + } + localeCacheLock.RUnlock() + + // Determine locale file path + var localePath string + _, container := os.LookupEnv("CONTAINER") + if container { + localePath = fmt.Sprintf("/app/locales/%s.json", lang) + } else { + localePath = fmt.Sprintf("./internal/locales/%s.json", lang) + } + + // Read locale file + data, err := os.ReadFile(localePath) + if err != nil { + // Fallback to English if locale file not found + if lang != "en" { + return loadLocale("en") + } + return nil, fmt.Errorf("failed to read locale file: %w", err) + } + + var translations map[string]string + if err := json.Unmarshal(data, &translations); err != nil { + return nil, fmt.Errorf("failed to parse locale file: %w", err) + } + + // Cache the translations + localeCacheLock.Lock() + localeCache[lang] = translations + localeCacheLock.Unlock() + + return translations, nil +} + +// getEmailTranslation gets a translation key from the locale, with fallback to English +func getEmailTranslation(lang, key string) string { + translations, err := loadLocale(lang) + if err != nil { + // Try English as fallback + if lang != "en" { + translations, err = loadLocale("en") + if err != nil { + return key // Return key if all else fails + } + } else { + return key + } + } + + if translation, ok := translations[key]; ok { + return translation + } + + // Fallback to English if key not found + if lang != "en" { + enTranslations, err := loadLocale("en") + if err == nil { + if enTranslation, ok := enTranslations[key]; ok { + return enTranslation + } + } + } + + return key +} + +// getEmailStyle returns the email style from environment variable, defaulting to "modern" +func getEmailStyle() string { + style := os.Getenv("emailStyle") + if style == "classic" { + return "classic" + } + return "modern" +} + // ******************************************************************* // * Unified Email Sending Function : * // ******************************************************************* @@ -1060,19 +1172,29 @@ func sendSMTPMessage(client *smtp.Client, from, to string, msg []byte) error { return nil } -// ******************************************************************* -// * sendBanAlert Function : * -// ******************************************************************* -func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, settings config.AppSettings) error { - subject := fmt.Sprintf("[Fail2Ban] %s: Banned %s from %s", jail, ip, hostname) +// renderClassicEmailDetails creates paragraph-based details for classic email template +func renderClassicEmailDetails(details []emailDetail) string { + if len(details) == 0 { + return `

No metadata available.

` + } + var b strings.Builder + for _, d := range details { + b.WriteString(`

` + html.EscapeString(d.Label) + `: ` + html.EscapeString(d.Value) + `

`) + b.WriteString("\n") + } + return b.String() +} - // Improved Responsive HTML Email - body := fmt.Sprintf(` +// buildClassicEmailBody creates the classic email template (original design with multilingual support) +func buildClassicEmailBody(title, intro string, details []emailDetail, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText, supportEmail string) string { + detailRows := renderClassicEmailDetails(details) + year := time.Now().Year() + return fmt.Sprintf(` -Fail2Ban Alert +%s + + +
+
+ + + +
+
+ +`, html.EscapeString(title), html.EscapeString(title), html.EscapeString(intro), html.EscapeString(intro), detailRows, html.EscapeString(whoisTitle), whoisHTML, html.EscapeString(logsTitle), logsHTML, html.EscapeString(footerText), year) +} + +func renderEmailDetails(details []emailDetail) string { + if len(details) == 0 { + return `

No metadata available.

` + } + var b strings.Builder + for _, d := range details { + b.WriteString(`

` + html.EscapeString(d.Label) + `: ` + html.EscapeString(d.Value) + `

`) + b.WriteString("\n") + } + return b.String() +} + +func formatWhoisForEmail(whois string, lang string, isModern bool) string { + noDataMsg := getEmailTranslation(lang, "email.whois.no_data") + if strings.TrimSpace(whois) == "" { + if isModern { + return `

` + html.EscapeString(noDataMsg) + `

` + } + return `
` + html.EscapeString(noDataMsg) + `
` + } + // Use
 to preserve all whitespace and newlines exactly as they are
+	if isModern {
+		return `
` + html.EscapeString(whois) + `
` + } + return `
` + html.EscapeString(whois) + `
` +} + +func formatLogsForEmail(ip, logs string, lang string, isModern bool) string { + noLogsMsg := getEmailTranslation(lang, "email.logs.no_data") + if strings.TrimSpace(logs) == "" { + if isModern { + return `

` + html.EscapeString(noLogsMsg) + `

` + } + return `
` + html.EscapeString(noLogsMsg) + `
` + } + + if isModern { + var b strings.Builder + b.WriteString(`
`) + lines := strings.Split(logs, "\n") + for _, line := range lines { + trimmed := strings.TrimRight(line, "\r") + if trimmed == "" { + continue + } + class := "email-log-line" + if isSuspiciousLogLineEmail(trimmed, ip) { + class = "email-log-line email-log-line-alert" + } + b.WriteString(`
` + html.EscapeString(trimmed) + `
`) + } + b.WriteString(`
`) + return b.String() + } + + // Classic format: simple pre tag + return `
` + html.EscapeString(logs) + `
` +} + +func isSuspiciousLogLineEmail(line, ip string) bool { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + return false + } + lowered := strings.ToLower(trimmed) + containsIP := ip != "" && strings.Contains(trimmed, ip) + statusCode := extractStatusCodeFromLine(trimmed) + hasBadStatus := statusCode >= 300 + hasIndicator := false + for _, indicator := range suspiciousLogIndicators { + if strings.Contains(lowered, indicator) { + hasIndicator = true + break + } + } + if containsIP { + return hasBadStatus || hasIndicator + } + return (hasBadStatus || hasIndicator) && ip == "" +} + +func extractStatusCodeFromLine(line string) int { + if match := httpQuotedStatusPattern.FindStringSubmatch(line); len(match) == 2 { + if code, err := strconv.Atoi(match[1]); err == nil { + return code + } + } + if match := httpPlainStatusPattern.FindStringSubmatch(line); len(match) == 2 { + if code, err := strconv.Atoi(match[1]); err == nil { + return code + } + } + return 0 +} + +// ******************************************************************* +// * sendBanAlert Function : * +// ******************************************************************* +func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, settings config.AppSettings) error { + lang := settings.Language + if lang == "" { + lang = "en" + } + + // Get translations + subject := fmt.Sprintf("[Fail2Ban] %s: %s %s %s %s", jail, + getEmailTranslation(lang, "email.ban.subject.banned"), + ip, + getEmailTranslation(lang, "email.ban.subject.from"), + hostname) + + details := []emailDetail{ + {Label: getEmailTranslation(lang, "email.ban.details.banned_ip"), Value: ip}, + {Label: getEmailTranslation(lang, "email.ban.details.jail"), Value: jail}, + {Label: getEmailTranslation(lang, "email.ban.details.hostname"), Value: hostname}, + {Label: getEmailTranslation(lang, "email.ban.details.failed_attempts"), Value: failures}, + {Label: getEmailTranslation(lang, "email.ban.details.country"), Value: country}, + {Label: getEmailTranslation(lang, "email.ban.details.timestamp"), Value: time.Now().UTC().Format(time.RFC3339)}, + } + + title := getEmailTranslation(lang, "email.ban.title") + intro := getEmailTranslation(lang, "email.ban.intro") + whoisTitle := getEmailTranslation(lang, "email.ban.whois_title") + logsTitle := getEmailTranslation(lang, "email.ban.logs_title") + footerText := getEmailTranslation(lang, "email.footer.text") + supportEmail := "support@swissmakers.ch" + + // Determine email style + emailStyle := getEmailStyle() + isModern := emailStyle == "modern" + + whoisHTML := formatWhoisForEmail(whois, lang, isModern) + logsHTML := formatLogsForEmail(ip, logs, lang, isModern) + + var body string + if isModern { + body = buildModernEmailBody(title, intro, details, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText) + } else { + body = buildClassicEmailBody(title, intro, details, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText, supportEmail) + } - // Send the email return sendEmail(settings.Destemail, subject, body, settings) } @@ -1149,10 +1487,51 @@ func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, set func TestEmailHandler(c *gin.Context) { settings := config.GetSettings() + lang := settings.Language + if lang == "" { + lang = "en" + } + + // Get translations + testDetails := []emailDetail{ + {Label: getEmailTranslation(lang, "email.test.details.recipient"), Value: settings.Destemail}, + {Label: getEmailTranslation(lang, "email.test.details.smtp_host"), Value: settings.SMTP.Host}, + {Label: getEmailTranslation(lang, "email.test.details.triggered_at"), Value: time.Now().Format(time.RFC1123)}, + } + + title := getEmailTranslation(lang, "email.test.title") + intro := getEmailTranslation(lang, "email.test.intro") + whoisTitle := getEmailTranslation(lang, "email.ban.whois_title") + logsTitle := getEmailTranslation(lang, "email.ban.logs_title") + footerText := getEmailTranslation(lang, "email.footer.text") + whoisNoData := getEmailTranslation(lang, "email.test.whois_no_data") + supportEmail := "support@swissmakers.ch" + + // Determine email style + emailStyle := getEmailStyle() + isModern := emailStyle == "modern" + + whoisHTML := `
` + html.EscapeString(whoisNoData) + `
` + if isModern { + whoisHTML = `

` + html.EscapeString(whoisNoData) + `

` + } + + sampleLogs := getEmailTranslation(lang, "email.test.sample_logs") + logsHTML := formatLogsForEmail("", sampleLogs, lang, isModern) + + var testBody string + if isModern { + testBody = buildModernEmailBody(title, intro, testDetails, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText) + } else { + testBody = buildClassicEmailBody(title, intro, testDetails, whoisHTML, logsHTML, whoisTitle, logsTitle, footerText, supportEmail) + } + + subject := getEmailTranslation(lang, "email.test.subject") + err := sendEmail( settings.Destemail, - "Test Email from Fail2Ban UI", - "This is a test email sent from the Fail2Ban UI to verify SMTP settings.", + subject, + testBody, settings, ) diff --git a/pkg/web/templates/index.html b/pkg/web/templates/index.html index ffdd580..6e90332 100644 --- a/pkg/web/templates/index.html +++ b/pkg/web/templates/index.html @@ -1033,21 +1033,41 @@
-
+
-

Ban Insights

-

Country distribution and recurring offenders.

+

Ban Insights

+

Country distribution and recurring offenders.

+ + +
+ + +
+ +
+
+
+

Bans by country

+

Top origins for the selected time range.

+
+ Geo +
+
+
-
-

Bans by country

-
-
- -
-

Recurring IPs

-
+ +
+
+
+

Recurring IPs

+

IP addresses repeatedly triggering Fail2ban.

+
+ Watchlist +
+
+
@@ -1309,6 +1329,18 @@ }); } + function formatNumber(value) { + var num = Number(value); + if (!isFinite(num)) { + return '0'; + } + try { + return num.toLocaleString(); + } catch (e) { + return String(num); + } + } + function withServerParam(url) { if (!currentServerId) { return url; @@ -2911,17 +2943,57 @@ function openBanInsightsModal() { var countriesContainer = document.getElementById('countryStatsContainer'); var recurringContainer = document.getElementById('recurringIPsContainer'); + var summaryContainer = document.getElementById('insightsSummary'); + + var totals = (latestBanInsights && latestBanInsights.totals) || { overall: 0, today: 0, week: 0 }; + if (summaryContainer) { + var summaryCards = [ + { + label: t('logs.overview.total_events', 'Total stored events'), + value: formatNumber(totals.overall || 0), + sub: t('logs.modal.total_overall_note', 'Lifetime bans recorded') + }, + { + label: t('logs.overview.total_today', 'Today'), + value: formatNumber(totals.today || 0), + sub: t('logs.modal.total_today_note', 'Last 24 hours') + }, + { + label: t('logs.overview.total_week', 'Last 7 days'), + value: formatNumber(totals.week || 0), + sub: t('logs.modal.total_week_note', 'Weekly activity') + } + ]; + summaryContainer.innerHTML = summaryCards.map(function(card) { + return '' + + '
' + + '

' + escapeHtml(card.label) + '

' + + '

' + escapeHtml(card.value) + '

' + + '

' + escapeHtml(card.sub) + '

' + + '
'; + }).join(''); + } var countries = (latestBanInsights && latestBanInsights.countries) || []; if (!countries.length) { countriesContainer.innerHTML = '

No bans recorded for this period.

'; } else { + var totalCountries = countries.reduce(function(sum, stat) { + return sum + (stat.count || 0); + }, 0) || 1; var countryHTML = countries.map(function(stat) { var label = stat.country || t('logs.overview.country_unknown', 'Unknown'); + var percent = Math.round(((stat.count || 0) / totalCountries) * 100); + percent = Math.min(Math.max(percent, 3), 100); return '' - + '
' - + ' ' + escapeHtml(label) + '' - + ' ' + (stat.count || 0) + '' + + '
' + + '
' + + ' ' + escapeHtml(label) + '' + + ' ' + formatNumber(stat.count || 0) + '' + + '
' + + '
' + + '
' + + '
' + '
'; }).join(''); countriesContainer.innerHTML = countryHTML; @@ -2935,16 +3007,17 @@ var countryLabel = stat.country || t('logs.overview.country_unknown', 'Unknown'); var lastSeenLabel = stat.lastSeen ? formatDateTime(stat.lastSeen) : '—'; return '' - + '
' + + '
' + '
' + '
' - + '

' + escapeHtml(stat.ip || '—') + '

' - + '

' + escapeHtml(countryLabel) + '

' - + '
' - + '
' - + '

' + (stat.count || 0) + '×

' - + '

' + t('logs.overview.last_seen', 'Last seen') + ': ' + escapeHtml(lastSeenLabel) + '

' + + '

' + escapeHtml(stat.ip || '—') + '

' + + '

' + escapeHtml(countryLabel) + '

' + '
' + + ' ' + formatNumber(stat.count || 0) + '×' + + '
' + + '
' + + ' ' + t('logs.overview.last_seen', 'Last seen') + '' + + ' ' + escapeHtml(lastSeenLabel) + '' + '
' + '
'; }).join('');