From 87d164f8f8159805e3b427bfe166bcefd2eb94e9 Mon Sep 17 00:00:00 2001 From: Michael Reber Date: Wed, 22 Jul 2026 20:41:49 +0200 Subject: [PATCH 1/7] Update fileheaders to correctly include all the AGLP v3 licence and fix some charset to be ASCII olny --- LICENSE | 147 +++++++++----------- README.md | 12 +- cmd/server/main.go | 7 +- deployment/container/README.md | 14 +- development/oidc/README.md | 6 +- docs/alert-providers.md | 22 +-- docs/architecture.md | 12 +- docs/configuration.md | 18 +-- docs/reverse-proxy.md | 4 +- docs/security.md | 4 +- docs/threat-intel.md | 2 +- docs/troubleshooting.md | 30 ++-- internal/auth/oidc.go | 4 +- internal/auth/session.go | 4 +- internal/config/fail2ban_bridge.go | 4 +- internal/config/logging.go | 4 +- internal/config/settings.go | 4 +- internal/config/settings_validation_test.go | 4 +- internal/enrichment/logparse.go | 9 +- internal/enrichment/patterns.go | 4 +- internal/enrichment/whoisparse.go | 6 +- internal/fail2ban/connector_agent.go | 6 +- internal/fail2ban/connector_agent_test.go | 16 +++ internal/fail2ban/connector_global.go | 4 +- internal/fail2ban/connector_local.go | 4 +- internal/fail2ban/connector_ssh.go | 15 +- internal/fail2ban/filter_management.go | 4 +- internal/fail2ban/jail_management.go | 9 +- internal/fail2ban/local_managed_files.go | 10 +- internal/fail2ban/manager.go | 6 +- internal/fail2ban/paths.go | 4 +- internal/fail2ban/paths_test.go | 4 +- internal/fail2ban/provider.go | 4 +- internal/fail2ban/variable_resolver.go | 4 +- internal/fail2ban/variable_resolver_test.go | 4 +- internal/integrations/mikrotik.go | 16 +++ internal/integrations/opnsense.go | 16 +++ internal/integrations/pfsense.go | 16 +++ internal/integrations/types.go | 33 +++-- internal/integrations/types_test.go | 16 +++ internal/shared/server.go | 4 +- internal/shared/validation.go | 4 +- internal/storage/storage.go | 13 +- internal/storage/storage_test.go | 4 +- internal/version/version.go | 4 +- pkg/web/advanced_actions.go | 12 +- pkg/web/assets.go | 4 +- pkg/web/assets_test.go | 4 +- pkg/web/auth.go | 8 +- pkg/web/authorization.go | 16 +++ pkg/web/basepath.go | 5 +- pkg/web/basepath_test.go | 4 +- pkg/web/callback_secret.go | 10 +- pkg/web/callback_secret_test.go | 4 +- pkg/web/console_logger.go | 4 +- pkg/web/email_header_test.go | 4 +- pkg/web/handlers.go | 92 +++++------- pkg/web/locales/ca.json | 10 +- pkg/web/locales/de.json | 12 +- pkg/web/locales/de_ch.json | 12 +- pkg/web/locales/en.json | 12 +- pkg/web/locales/es.json | 12 +- pkg/web/locales/fr.json | 24 ++-- pkg/web/locales/it.json | 16 +-- pkg/web/locales/ja.json | 10 +- pkg/web/locales/zh.json | 12 +- pkg/web/outbound_http.go | 4 +- pkg/web/routes.go | 4 +- pkg/web/secret_masking.go | 4 +- pkg/web/secret_masking_test.go | 4 +- pkg/web/smtp_tls.go | 4 +- pkg/web/smtp_tls_test.go | 4 +- pkg/web/static/js/console.js | 6 +- pkg/web/static/js/core.js | 9 +- pkg/web/static/js/dashboard.js | 56 +++----- pkg/web/static/js/ignoreips.js | 2 +- pkg/web/static/js/init.js | 3 - pkg/web/static/js/jails.js | 9 +- pkg/web/static/js/modals.js | 7 +- pkg/web/static/js/servers.js | 4 +- pkg/web/static/js/settings.js | 1 - pkg/web/static/js/threat-intel.js | 18 +-- pkg/web/templates/index.html | 17 +-- pkg/web/websocket.go | 7 +- pkg/web/whois.go | 4 +- 85 files changed, 500 insertions(+), 495 deletions(-) diff --git a/LICENSE b/LICENSE index f288702..be3f7b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by + it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md index 4314c53..40d21c5 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,15 @@ **Enterprise-Grade Intrusion Detection System Management Platform** -[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPLv3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![Go Version](https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go)](https://golang.org/) [![Platform](https://img.shields.io/badge/Platform-Linux-lightgrey)](https://www.linux.org/) Fail2Ban UI is a management platform for operating Fail2Ban across one or more Linux hosts. It provides a central place to review bans, search and unban IP addresses, manage jails and filters, and receive notifications. -The project is maintained by Swissmakers GmbH and released under GPL-3.0. +The project is maintained by Swissmakers GmbH and released under AGPL-3.0. -[Quick start](#quick-start-container) • [Documentation](#documentation) • [Configuration reference](docs/configuration.md) • [Architecture](docs/architecture.md) • [Screenshots](#screenshots) +[Quick start](#quick-start-container) - [Documentation](#documentation) - [Configuration reference](docs/configuration.md) - [Architecture](docs/architecture.md) - [Screenshots](#screenshots) ## What this project does @@ -70,7 +70,7 @@ podman run -d --name fail2ban-ui --network=host \ ### Verification 1. Open `http://localhost:8080`. -2. In the UI, go to **Settings → Manage Servers**, enable the local connector, and click **Test connection**. +2. In the UI, go to **Settings -> Manage Servers**, enable the local connector, and click **Test connection**. ### Next steps @@ -140,7 +140,7 @@ When clicking on "Edit Filter / Jail" the Jail configuration editor is opened. I #### Logpath Test ![Logpath Test](screenshots/1.3_Dashboard_Manage_Jails_Edit_Logpathtest.png) -Logpath testing functionality that verifies log file paths and checks if files are accessible. Shows test results with visual indicators (✓/✗) for each log path. +Logpath testing functionality that verifies log file paths and checks if files are accessible. Shows test results with visual indicators (yes/no) for each log path. #### Create new Filter ![Create Filter](screenshots/1.4_Dashboard_Manage_Jails_Create_Filter.png) @@ -212,4 +212,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details. ## License -GPL-3.0. See [LICENSE](LICENSE). +AGPL-3.0. See [LICENSE](LICENSE). diff --git a/cmd/server/main.go b/cmd/server/main.go index 66cafcd..b46f1f5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -109,14 +109,12 @@ func main() { log.Println("WARNING: OIDC authentication is DISABLED -> Run this way only in a trusted network or behind an authenticating reverse proxy (see docs/security.md).") } - // Set Gin mode if settings.Debug { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } - // Initialize router router := gin.Default() serverPort := strconv.Itoa(int(settings.Port)) bindAddress, _ := config.GetBindAddressFromEnv() @@ -135,7 +133,6 @@ func main() { web.SetConsoleLogEnabled(enabled) }) - // Register routes web.RegisterRoutes(router, wsHub) isLOTRMode := config.IsLOTRModeActive(settings.AlertCountries) printWelcomeBanner(bindAddress, serverPort, isLOTRMode) diff --git a/deployment/container/README.md b/deployment/container/README.md index c222ee0..5faa889 100644 --- a/deployment/container/README.md +++ b/deployment/container/README.md @@ -177,7 +177,7 @@ For the full list, including `CALLBACK_URL`, `CALLBACK_SECRET`, `BASE_PATH`, and ### First launch 1. **Open the web interface** at `http://localhost:8080` (or your configured port). -2. **Add your first server** under **Settings → Manage Servers**: +2. **Add your first server** under **Settings -> Manage Servers**: - Local server: enable the local connector if Fail2Ban runs on the same host. - Remote server: add a server over SSH or through an API agent. 3. **Configure the settings** @@ -269,10 +269,10 @@ Characteristics: Volume structure: ``` -./fail2ban-config/fail2ban → /config/fail2ban (fail2ban container) -./fail2ban-config/fail2ban → /etc/fail2ban (fail2ban-ui container) -./f2b-run → /var/run/fail2ban (both containers) -./config → /config (fail2ban-ui container) +./fail2ban-config/fail2ban -> /config/fail2ban (fail2ban container) +./fail2ban-config/fail2ban -> /etc/fail2ban (fail2ban-ui container) +./f2b-run -> /var/run/fail2ban (both containers) +./config -> /config (fail2ban-ui container) ``` **Important:** @@ -295,7 +295,7 @@ Modern distributions - Rocky Linux 9+, RHEL 9+, Fedora 36+, Debian 12+ - use **n Solution - switch Fail2Ban to nftables-based actions: 1. Open the Fail2Ban UI web interface. -2. Go to **Settings → Fail2Ban Settings**. +2. Go to **Settings -> Fail2Ban Settings**. 3. Change **Banaction** from `iptables-multiport` to `nftables-multiport`. 4. Change **Banaction Allports** from `iptables-allports` to `nftables-allports`. 5. Save; Fail2Ban reloads automatically. @@ -396,7 +396,7 @@ If Fail2Ban fails to ban IPs with errors related to iptables or multiport extens Symptoms: empty dashboard, no servers visible. -1. Go to **Settings → Manage Servers** in the web UI. +1. Go to **Settings -> Manage Servers** in the web UI. 2. Enable the **Local Connector** if Fail2Ban runs locally. 3. Add a remote server over SSH or an API agent. 4. Verify the server connection status. diff --git a/development/oidc/README.md b/development/oidc/README.md index 0090f5e..c772f7f 100644 --- a/development/oidc/README.md +++ b/development/oidc/README.md @@ -106,7 +106,7 @@ podman compose up -d rm -rf config/keycloak-client-secret podman compose up -d ``` - Alternatively, update the client manually in the Keycloak admin console: **Clients → fail2ban-ui**, then adjust "Valid redirect URIs" and "Valid post logout redirect URIs" and save. + Alternatively, update the client manually in the Keycloak admin console: **Clients -> fail2ban-ui**, then adjust "Valid redirect URIs" and "Valid post logout redirect URIs" and save. ## Setup @@ -186,7 +186,7 @@ If the automatic configuration fails, create the client by hand: podman compose restart fail2ban-ui ``` 7. **Create a test user** (recommended): - - **Users → Create new user**, set a username (for example `testuser`) and optionally an email. Click **Create**. + - **Users -> Create new user**, set a username (for example `testuser`) and optionally an email. Click **Create**. - On the **Credentials** tab, set a password and toggle **Temporary** off, so no password reset is forced on first login. Save. Then open `http://localhost:3080`: you are redirected to the Keycloak login, and after authentication back to Fail2Ban UI. @@ -292,7 +292,7 @@ Provider-specific issuer URLs: - Logs: `podman logs DEV_keycloak` - Port mapping: `netstat -tlnp | grep 3000` - The database must be ready first (its own health check). -- Keycloak needs 30–60 seconds; wait for "Keycloak started" in the logs. +- Keycloak needs 30-60 seconds; wait for "Keycloak started" in the logs. - Keycloak 26+ serves health endpoints on port 9000: ```bash # From the host (if port 9000 is exposed): diff --git a/docs/alert-providers.md b/docs/alert-providers.md index ccd6fb1..4397ec7 100644 --- a/docs/alert-providers.md +++ b/docs/alert-providers.md @@ -304,15 +304,15 @@ PUT _index_template/fail2ban #### 2. Create an API key -In Kibana: **Stack Management → API Keys → Create API key**. The key needs write access to the `fail2ban-events-`* indices. +In Kibana: **Stack Management -> API Keys -> Create API key**. The key needs write access to the `fail2ban-events-`* indices. #### 3. Configure Fail2Ban UI -Enter the Elasticsearch URL, index name, and API key under **Settings → Alert Settings**. Save and click **Test Connection** to verify; the test creates the first document. +Enter the Elasticsearch URL, index name, and API key under **Settings -> Alert Settings**. Save and click **Test Connection** to verify; the test creates the first document. #### 4. Create a Kibana data view -In Kibana: **Stack Management → Data Views → Create data view**. Use `fail2ban-events-`* as the name and index pattern, and select `@timestamp` as the time field. +In Kibana: **Stack Management -> Data Views -> Create data view**. Use `fail2ban-events-`* as the name and index pattern, and select `@timestamp` as the time field. #### 5. Explore in Discover @@ -342,14 +342,14 @@ When a ban or unban event arrives through the Fail2Ban callback and payload vali ``` Ban/unban event - → store in DB + WebSocket broadcast - → alerts enabled for this event type? - → country filter matches? - → dispatch to provider: - ├── email → sendBanAlert() → sendEmail() via SMTP - ├── webhook → sendWebhookAlert() → HTTP POST/PUT - └── elasticsearch → enrich logs (grok) + enrich whois (regex) - → sendElasticsearchAlert() → POST //_doc + -> store in DB + WebSocket broadcast + -> alerts enabled for this event type? + -> country filter matches? + -> dispatch to provider: + +-- email -> sendBanAlert() -> sendEmail() via SMTP + +-- webhook -> sendWebhookAlert() -> HTTP POST/PUT + \-- elasticsearch -> enrich logs (grok) + enrich whois (regex) + -> sendElasticsearchAlert() -> POST //_doc ``` Switching providers does not affect event storage or WebSocket broadcasting; only the notification delivery channel changes. diff --git a/docs/architecture.md b/docs/architecture.md index f76407e..cfe6924 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -97,12 +97,12 @@ The browser communicates with the backend over HTTPS (REST) and a WebSocket conn | Path | Protocol / port | Direction | Authentication | | ------------------------- | ---------------------------------------------------------------- | ---------------- | ---------------------------------- | -| Browser → Fail2ban-UI | HTTPS / WSS, port 8080 by default (place behind a reverse proxy) | inbound to Fail2ban-UI | OIDC session (optional) | -| Fail2ban-UI → SSH-connected host | SSH, port 22 | outbound from Fail2ban-UI | SSH key, dedicated service account | -| Fail2ban-UI → agent-connected host | HTTP(S), agent port | outbound from Fail2ban-UI | Agent token | -| Fail2Ban host → Fail2ban-UI | HTTP(S) `POST /api/ban`, `/api/unban` | inbound to Fail2ban-UI | `X-Callback-Secret` header | -| Fail2ban-UI → alert providers | SMTP / HTTPS / Elasticsearch API | outbound from Fail2ban-UI | Provider-specific | -| Fail2ban-UI → edge firewall | SSH (MikroTik) or HTTPS (pfSense, OPNsense) | outbound from Fail2ban-UI | Device credentials / API token | +| Browser -> Fail2ban-UI | HTTPS / WSS, port 8080 by default (place behind a reverse proxy) | inbound to Fail2ban-UI | OIDC session (optional) | +| Fail2ban-UI -> SSH-connected host | SSH, port 22 | outbound from Fail2ban-UI | SSH key, dedicated service account | +| Fail2ban-UI -> agent-connected host | HTTP(S), agent port | outbound from Fail2ban-UI | Agent token | +| Fail2Ban host -> Fail2ban-UI | HTTP(S) `POST /api/ban`, `/api/unban` | inbound to Fail2ban-UI | `X-Callback-Secret` header | +| Fail2ban-UI -> alert providers | SMTP / HTTPS / Elasticsearch API | outbound from Fail2ban-UI | Provider-specific | +| Fail2ban-UI -> edge firewall | SSH (MikroTik) or HTTPS (pfSense, OPNsense) | outbound from Fail2ban-UI | Device credentials / API token | **Important:** Do not expose the Fail2ban-UI directly to the public Internet. Place it behind a reverse proxy, VPN, or firewall rules, and enable OIDC where possible. The callback endpoint is the only path that managed hosts must be able to reach. diff --git a/docs/configuration.md b/docs/configuration.md index 9c2f83a..f7a6f12 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -70,7 +70,7 @@ With a subpath: SSH-connected servers can enable **reverse tunnel for events** (server form). The UI then opens a reverse tunnel (`ssh -R :localhost:`) alongside the SSH control connection so callbacks reach the UI even when the managed host cannot connect to it directly (NAT, firewall). The port is derived from `CALLBACK_URL` (explicit port, otherwise 443/80 by scheme). -The tunnel is only used if `CALLBACK_URL` points to `localhost`/`127.0.0.1` — the remote Fail2Ban sends its callbacks to that URL, which the tunnel forwards to the UI. With a public callback URL the callbacks bypass the tunnel; the UI logs a warning in that case. Note that a localhost callback URL applies globally, so mixing tunneled and non-tunneled remote servers is not possible. +The tunnel is only used if `CALLBACK_URL` points to `localhost`/`127.0.0.1` - the remote Fail2Ban sends its callbacks to that URL, which the tunnel forwards to the UI. With a public callback URL the callbacks bypass the tunnel; the UI logs a warning in that case. Note that a localhost callback URL applies globally, so mixing tunneled and non-tunneled remote servers is not possible. ## Privacy and telemetry controls @@ -93,33 +93,33 @@ The tunnel is only used if `CALLBACK_URL` points to `localhost`/`127.0.0.1` — ## Global Fail2Ban defaults (UI-managed) -Configure under **Settings → Global Settings**. These values are written to the `[DEFAULT]` section of the managed `jail.local` and pushed to every managed host: +Configure under **Settings -> Global Settings**. These values are written to the `[DEFAULT]` section of the managed `jail.local` and pushed to every managed host: * `bantime`, `findtime`, `maxretry`, `ignoreip`, `banaction` / `banaction_allports`, firewall `chain` * Bantime increment escalation (optional, only emitted when set): - * `bantime.rndtime` — random jitter added to escalating bans - * `bantime.maxtime` — cap for escalating bans (e.g. `5w`) - * `bantime.factor` — escalation multiplier (Fail2Ban default: `1`) - * `bantime.overalljails` — count repeat offenses across all jails instead of per jail + * `bantime.rndtime` - random jitter added to escalating bans + * `bantime.maxtime` - cap for escalating bans (e.g. `5w`) + * `bantime.factor` - escalation multiplier (Fail2Ban default: `1`) + * `bantime.overalljails` - count repeat offenses across all jails instead of per jail Duration fields accept plain seconds or Fail2Ban time suffixes (`3600`, `48h`, `5w`, `1d 12h`). `bantime` additionally accepts `-1` for permanent bans. ## Alert settings (UI-managed) -Configure under **Settings → Alert Settings**: +Configure under **Settings -> Alert Settings**: * Provider: `email`, `webhook`, or `elasticsearch` * Enable alerts for bans and/or unbans * Alert country filters * GeoIP provider and log-line limits -> **Privacy note on the `builtin` GeoIP provider:** it resolves countries via the free ip-api.com service, which means every enriched (banned) IP address is sent to a third party — and the free tier only supports plain HTTP, so the queries travel unencrypted. For privacy-sensitive deployments use the MaxMind provider with a local GeoLite2 database instead. +> **Privacy note on the `builtin` GeoIP provider:** it resolves countries via the free ip-api.com service, which means every enriched (banned) IP address is sent to a third party - and the free tier only supports plain HTTP, so the queries travel unencrypted. For privacy-sensitive deployments use the MaxMind provider with a local GeoLite2 database instead. For provider behavior and payloads, see [alert-providers.md](alert-providers.md) and [webhooks.md](webhooks.md). ## Threat intelligence settings (UI-managed) -Configure under **Settings → Alert Settings**: +Configure under **Settings -> Alert Settings**: * `threatIntel.provider`: `none`, `alienvault`, or `abuseipdb` * `threatIntel.alienVaultApiKey` (for `alienvault`) diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 64c74ec..053ee72 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -34,13 +34,13 @@ For correct behavior, including WebSocket live updates, the proxy must: 2. Allow WebSocket upgrades on the real-time endpoint: `GET /api/ws` at the site root, or `GET {BASE_PATH}/api/ws` when using a subpath. 3. Forward client IP context: `X-Forwarded-For` and `X-Forwarded-Proto`. -**Path-prefix handling:** If the proxy *strips* a path prefix before forwarding (external `/myf2b/` → upstream `/`), leave `BASE_PATH` unset and configure the application as if it lived at the root; only the public URLs change. If the application receives the *full* path including `/myf2b`, set `BASE_PATH=/myf2b` and forward the prefix unchanged. +**Path-prefix handling:** If the proxy *strips* a path prefix before forwarding (external `/myf2b/` -> upstream `/`), leave `BASE_PATH` unset and configure the application as if it lived at the root; only the public URLs change. If the application receives the *full* path including `/myf2b`, set `BASE_PATH=/myf2b` and forward the prefix unchanged. ## Subpath deployment (`BASE_PATH`) When Fail2Ban UI runs with `BASE_PATH=/myf2b` (see [configuration.md](configuration.md)): -* The proxy `location` must match the prefix and pass the *same* path to the backend, with no strip: `https://host/myf2b/api/version` → upstream `http://127.0.0.1:8080/myf2b/api/version`. +* The proxy `location` must match the prefix and pass the *same* path to the backend, with no strip: `https://host/myf2b/api/version` -> upstream `http://127.0.0.1:8080/myf2b/api/version`. * The WebSocket URL in the browser becomes `wss://host/myf2b/api/ws`. * `CALLBACK_URL` and `OIDC_REDIRECT_URL` must include `/myf2b`, as described in the configuration reference. diff --git a/docs/security.md b/docs/security.md index 8df934f..1e13974 100644 --- a/docs/security.md +++ b/docs/security.md @@ -17,9 +17,9 @@ See [reverse-proxy.md](reverse-proxy.md) for hardened proxy examples and the Web Sessions are stateless encrypted cookies (AES-GCM), logout clears the cookie but cannot revoke an already-captured session token before its expiry (`OIDC_SESSION_MAX_AGE`, default 1 hour). Keep session lifetimes short and always serve the UI over TLS. -When OIDC role-based access control is configured (`OIDC_ADMIN_ROLES` / `OIDC_SUPPORT_ROLES`), the user's roles and access level are captured at login and stored in the session cookie. Role changes at the identity provider only take effect after the user logs in again — another reason to keep session lifetimes short. Users whose roles match neither list can authenticate but are denied on every API endpoint. +When OIDC role-based access control is configured (`OIDC_ADMIN_ROLES` / `OIDC_SUPPORT_ROLES`), the user's roles and access level are captured at login and stored in the session cookie. Role changes at the identity provider only take effect after the user logs in again - another reason to keep session lifetimes short. Users whose roles match neither list can authenticate but are denied on every API endpoint. -The debug console (Settings → Console Output) mirrors the complete server log to every connected UI client over the WebSocket. Log lines can include client IPs, email addresses, and configuration diagnostics — leave it disabled unless actively debugging. +The debug console (Settings -> Console Output) mirrors the complete server log to every connected UI client over the WebSocket. Log lines can include client IPs, email addresses, and configuration diagnostics - leave it disabled unless actively debugging. ## Input validation diff --git a/docs/threat-intel.md b/docs/threat-intel.md index 30e0ec0..f4c836a 100644 --- a/docs/threat-intel.md +++ b/docs/threat-intel.md @@ -23,7 +23,7 @@ Fail2Ban UI can query external threat-intelligence sources for suspicious IPs. T ## Configuration -Under **Settings → Alert Settings**: +Under **Settings -> Alert Settings**: 1. If you do not have an API key yet, create one with the selected provider. 2. Set **Threat Intel Provider** to `AlienVault OTX` or `AbuseIPDB`. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 969d0f5..7148b2c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -92,7 +92,7 @@ cat /etc/fail2ban/action.d/ui-custom-action.conf # to your callback URL, for example http://10.88.0.1:8080/api/ban ``` -If the file does not exist or looks wrong, go to **Settings → Manage Servers** in the UI, select the server, and click **Test connection**. The UI re-deploys the action file automatically for local connectors. +If the file does not exist or looks wrong, go to **Settings -> Manage Servers** in the UI, select the server, and click **Test connection**. The UI re-deploys the action file automatically for local connectors. ### Step 2: Verify jail.local references the action @@ -139,7 +139,7 @@ Then trigger a test ban and check `audit.log` or `sealert` again. This is the sa ### Step 4: Verify the callback secret -Every callback must include the `X-Callback-Secret` header, and the value must match what the UI expects. The current secret is visible under **Settings → General Settings → Callback Secret** (with a show/hide toggle), or in the container environment. +Every callback must include the `X-Callback-Secret` header, and the value must match what the UI expects. The current secret is visible under **Settings -> General Settings -> Callback Secret** (with a show/hide toggle), or in the container environment. ```bash # Secret used by the action file: @@ -253,7 +253,7 @@ With debug mode enabled in the UI settings, the raw JSON body of every incoming The callback payload includes a `serverId`, which the UI uses to match the event to a configured server. If it matches no known server, the callback is rejected. -Check that the `serverId` in the action file matches the server ID shown under **Settings → Manage Servers**: +Check that the `serverId` in the action file matches the server ID shown under **Settings -> Manage Servers**: ```bash curl -s http://$FAIL2BAN_UI_HOST:8080/api/servers \ @@ -264,15 +264,15 @@ curl -s http://$FAIL2BAN_UI_HOST:8080/api/servers \ ``` Fail2Ban detects an intrusion - → triggers actionban in ui-custom-action.conf - → curl POST /api/ban with JSON payload + X-Callback-Secret header - → Fail2Ban UI validates the secret - → validates the IP format - → resolves the server (by serverId) - → stores the event in SQLite (ban_events) - → broadcasts over WebSocket to all connected browsers - → optional: dispatches an alert (Email / Webhook / Elasticsearch) - → optional: evaluates advanced actions (recurring offenders) + -> triggers actionban in ui-custom-action.conf + -> curl POST /api/ban with JSON payload + X-Callback-Secret header + -> Fail2Ban UI validates the secret + -> validates the IP format + -> resolves the server (by serverId) + -> stores the event in SQLite (ban_events) + -> broadcasts over WebSocket to all connected browsers + -> optional: dispatches an alert (Email / Webhook / Elasticsearch) + -> optional: evaluates advanced actions (recurring offenders) ``` If any step fails, the chain stops and the event does not appear in the UI. @@ -297,7 +297,7 @@ The symptom "unban works, but the UI only records unbans for *new* blocks" there ### Alerts not sent (any provider) -1. Verify that alerts are enabled for the event type (ban and/or unban) under **Settings → Alert Settings**. +1. Verify that alerts are enabled for the event type (ban and/or unban) under **Settings -> Alert Settings**. 2. Check which alert provider is selected, and re-check all settings of the active provider. 3. Check the country filter: if specific countries are selected, only IPs geolocated to those countries trigger alerts. Set it to `ALL` to alert on every event. 4. Confirm the dispatch in the Fail2Ban UI logs (enable debug logging for more detail): @@ -343,7 +343,7 @@ curl -v -X POST https://your-webhook-url \ Common causes: * **Connection refused / timeout.** Verify that the Elasticsearch URL is reachable from the Fail2Ban UI host. -* **401 Unauthorized.** API key or credentials are incorrect. Verify the key in Kibana under **Stack Management → API Keys**. +* **401 Unauthorized.** API key or credentials are incorrect. Verify the key in Kibana under **Stack Management -> API Keys**. * **403 Forbidden.** The API key lacks write permissions on the target index. Create a key with `write` and `create_index` privileges for `fail2ban-events-*`. * **Index template missing.** Without a template, Elasticsearch uses dynamic mapping, which may produce suboptimal field types. Create the template as described in [alert-providers.md](alert-providers.md#elasticsearch-setup). @@ -391,7 +391,7 @@ podman logs fail2ban-ui If real-time dashboard updates - ban and unban events appearing without a page refresh - do not work, check: -* The browser console for WebSocket errors (F12 → Console). +* The browser console for WebSocket errors (F12 -> Console). * The WebSocket status indicator in the UI footer. * The reverse proxy, if one is used, supports WebSocket upgrades. diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index 0ea5490..008c492 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -2,11 +2,11 @@ // // Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/auth/session.go b/internal/auth/session.go index 2c9a22b..8035630 100644 --- a/internal/auth/session.go +++ b/internal/auth/session.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/config/fail2ban_bridge.go b/internal/config/fail2ban_bridge.go index be34c05..f5f8cfb 100644 --- a/internal/config/fail2ban_bridge.go +++ b/internal/config/fail2ban_bridge.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/config/logging.go b/internal/config/logging.go index 1db7a1e..73355ec 100644 --- a/internal/config/logging.go +++ b/internal/config/logging.go @@ -2,11 +2,11 @@ // // Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/config/settings.go b/internal/config/settings.go index 8706ea4..b4b7354 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/config/settings_validation_test.go b/internal/config/settings_validation_test.go index 8122595..d6cce2b 100644 --- a/internal/config/settings_validation_test.go +++ b/internal/config/settings_validation_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/enrichment/logparse.go b/internal/enrichment/logparse.go index 537def0..5a43604 100644 --- a/internal/enrichment/logparse.go +++ b/internal/enrichment/logparse.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -56,11 +56,11 @@ func compileAll(defs []PatternDef) []compiledPattern { for _, d := range defs { g := grok.New() if err := g.AddPatterns(SubPatterns); err != nil { - log.Printf("⚠️ enrichment failed to add sub-patterns for %s: %v", d.Name, err) + log.Printf("WARNING: enrichment failed to add sub-patterns for %s: %v", d.Name, err) continue } if err := g.Compile(d.Pattern, true); err != nil { - log.Printf("⚠️ enrichment failed to compile pattern %s: %v", d.Name, err) + log.Printf("WARNING: enrichment failed to compile pattern %s: %v", d.Name, err) continue } out = append(out, compiledPattern{def: d, grok: g}) @@ -136,7 +136,6 @@ func ParseLogLines(logs, jail string) map[string]interface{} { // Helper functions // ========================================================================= -// Splits the raw log text func splitAndClean(logs string) []string { raw := strings.Split(logs, "\n") var out []string diff --git a/internal/enrichment/patterns.go b/internal/enrichment/patterns.go index 4561ab8..499463a 100644 --- a/internal/enrichment/patterns.go +++ b/internal/enrichment/patterns.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/enrichment/whoisparse.go b/internal/enrichment/whoisparse.go index 04611e7..95aa044 100644 --- a/internal/enrichment/whoisparse.go +++ b/internal/enrichment/whoisparse.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -111,7 +111,7 @@ func ParseWhois(whois string) map[string]interface{} { seenKeys[outField] = true } - // Normalises the ASN: strips "AS" prefix if present (e.g. "AS200373" → "200373") + // Normalises the ASN: strips "AS" prefix if present (e.g. "AS200373" -> "200373") if asn, ok := result["whois.asn"].(string); ok { asn = strings.TrimSpace(asn) if strings.HasPrefix(strings.ToUpper(asn), "AS") { diff --git a/internal/fail2ban/connector_agent.go b/internal/fail2ban/connector_agent.go index ff5c6fc..2610b3c 100644 --- a/internal/fail2ban/connector_agent.go +++ b/internal/fail2ban/connector_agent.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -395,7 +395,7 @@ func (ac *AgentConnector) do(req *http.Request, out any) error { preview := trimmed if len(preview) > 512 { - preview = preview[:512] + "…" + preview = preview[:512] + "..." } debugf("Agent response [%s]: %s | %s", ac.server.Name, resp.Status, preview) diff --git a/internal/fail2ban/connector_agent_test.go b/internal/fail2ban/connector_agent_test.go index 36b0210..63795e3 100644 --- a/internal/fail2ban/connector_agent_test.go +++ b/internal/fail2ban/connector_agent_test.go @@ -1,3 +1,19 @@ +// Fail2ban UI - A Swiss made, management interface for Fail2ban. +// +// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) +// +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.gnu.org/licenses/agpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package fail2ban import ( diff --git a/internal/fail2ban/connector_global.go b/internal/fail2ban/connector_global.go index b8cf51d..f6508a6 100644 --- a/internal/fail2ban/connector_global.go +++ b/internal/fail2ban/connector_global.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/fail2ban/connector_local.go b/internal/fail2ban/connector_local.go index e8c3be7..cc9f2d8 100644 --- a/internal/fail2ban/connector_local.go +++ b/internal/fail2ban/connector_local.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/fail2ban/connector_ssh.go b/internal/fail2ban/connector_ssh.go index 493c174..055f6e1 100644 --- a/internal/fail2ban/connector_ssh.go +++ b/internal/fail2ban/connector_ssh.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -316,12 +316,10 @@ func (sc *SSHConnector) ensureAction(ctx context.Context) error { debugf("SSH ensureAction command [%s]: ssh %s (with here-doc via stdin)", sc.server.Name, strings.Join(args, " ")) - // Capture stdout and stderr var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr - // Start the command if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start ssh command: %w", err) } @@ -405,7 +403,7 @@ func (sc *SSHConnector) runFail2banCommand(ctx context.Context, args ...string) return sc.runRemoteCommand(ctx, cmdArgs) } -// Detects "no systemd” situations on the remote host or if an interactive authentication is required. +// Detects "no systemd" situations on the remote host or if an interactive authentication is required. func (sc *SSHConnector) isSystemctlUnavailable(output string, err error) bool { msg := strings.ToLower(output + " " + err.Error()) return strings.Contains(msg, "command not found") || @@ -446,7 +444,6 @@ func (sc *SSHConnector) runRemoteCommand(ctx context.Context, command []string) Pgid: 0, } debugf("SSH command [%s]: ssh %s", sc.server.Name, strings.Join(args, " ")) - // Capture stdout and stderr var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr @@ -991,7 +988,6 @@ func (sc *SSHConnector) resolveFilterIncludesRemote(ctx context.Context, filterC for i, line := range lines { trimmed := strings.TrimSpace(line) - // Check for [INCLUDES] section if strings.HasPrefix(trimmed, "[INCLUDES]") { inIncludesSection = true continue @@ -1103,7 +1099,6 @@ func (sc *SSHConnector) resolveFilterIncludesRemote(ctx context.Context, filterC // Note: Self-inclusion in "after" directive is intentional in fail2ban // (e.g., after = apache-common.local is standard pattern for .local files) - // Self-inclusion in "after" is intentional (e.g. after = apache-common.local). contentStr, err := readRemoteFilterFile(baseName) if err != nil { @@ -1145,7 +1140,6 @@ func (sc *SSHConnector) TestFilter(ctx context.Context, filterName string, logLi var script string if filterContent != "" { - // Resolve filter includes locally. filterDPath := filepath.Join(fail2banPath, "filter.d") resolvedContent, err := sc.resolveFilterIncludesRemote(ctx, filterContent, filterDPath, filterName) if err != nil { @@ -1438,7 +1432,6 @@ except Exception as e: PYEOF `, originalPath) - // Run resolution script resolveOut, err := sc.runRemoteCommand(ctx, []string{resolveScript}) if err != nil { return originalPath, "", nil, fmt.Errorf("failed to resolve variables: %w", err) @@ -1492,7 +1485,7 @@ func (sc *SSHConnector) EnsureJailLocalStructure(ctx context.Context) error { // Run experimental migration if enabled if isJailAutoMigrationEnabled() { - debugf("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration for SSH server %s", sc.server.Name) + debugf("JAIL_AUTOMIGRATION=true: running experimental jail.local -> jail.d/ migration for SSH server %s", sc.server.Name) if err := sc.MigrateJailsFromJailLocalRemote(ctx); err != nil { return fmt.Errorf("failed to migrate legacy jails from jail.local on remote server %s: %w", sc.server.Name, err) } diff --git a/internal/fail2ban/filter_management.go b/internal/fail2ban/filter_management.go index b7868c2..2faa873 100644 --- a/internal/fail2ban/filter_management.go +++ b/internal/fail2ban/filter_management.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/fail2ban/jail_management.go b/internal/fail2ban/jail_management.go index 5702527..cf1d5da 100644 --- a/internal/fail2ban/jail_management.go +++ b/internal/fail2ban/jail_management.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -128,7 +128,6 @@ func ValidateJailName(name string) error { return fmt.Errorf("jail name '%s' is reserved and cannot be used", name) } - // Check for invalid characters if invalidNameChars.MatchString(name) { return fmt.Errorf("jail name '%s' contains invalid characters. Only alphanumeric characters, dashes, and underscores are allowed", name) } @@ -162,7 +161,6 @@ func ListJailFiles(directory string) ([]string, error) { continue } - // Only include .local and .conf files if strings.HasSuffix(name, ".local") || strings.HasSuffix(name, ".conf") { fullPath := filepath.Join(directory, name) files = append(files, fullPath) @@ -340,7 +338,7 @@ func GetAllJails(configPath string) ([]JailInfo, error) { // Run migration once if enabled (experimental, off by default) if isJailAutoMigrationEnabled() { migrationOnce.Do(func() { - debugf("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration") + debugf("JAIL_AUTOMIGRATION=true: running experimental jail.local -> jail.d/ migration") if err := MigrateJailsFromJailLocal(configPath); err != nil { debugf("Migration warning: %v", err) } @@ -426,7 +424,6 @@ func UpdateJailEnabledStates(updates map[string]bool, configPath string) error { } debugf("Processing jail: %s, enabled: %t", jailName, enabled) - // Ensure .local file exists if err := ensureJailLocalFile(jailName, configPath); err != nil { return fmt.Errorf("failed to ensure .local file for jail %s: %w", jailName, err) } diff --git a/internal/fail2ban/local_managed_files.go b/internal/fail2ban/local_managed_files.go index 8dd34e3..c3b2177 100644 --- a/internal/fail2ban/local_managed_files.go +++ b/internal/fail2ban/local_managed_files.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -50,7 +50,7 @@ func EnsureManagedJailLocal(configPath string, content []byte) error { jailPath := JailLocal(configPath) rootDir := NormalizeConfigPath(configPath) if _, err := os.Stat(filepath.Dir(jailPath)); os.IsNotExist(err) { - return fmt.Errorf("fail2ban configuration directory does not exist at %s — install fail2ban or set the correct configuration path for this server", rootDir) + return fmt.Errorf("fail2ban configuration directory does not exist at %s - install fail2ban or set the correct configuration path for this server", rootDir) } var existingContent string fileExists := false @@ -76,7 +76,7 @@ func WriteLocalActionFile(configPath, callbackURL, serverID string) error { actionPath := CustomActionFile(configPath) actionDir := ActionDir(configPath) if _, err := os.Stat(actionDir); os.IsNotExist(err) { - return fmt.Errorf("fail2ban action.d directory does not exist at %s — install fail2ban or set the correct configuration path for this server", actionDir) + return fmt.Errorf("fail2ban action.d directory does not exist at %s - install fail2ban or set the correct configuration path for this server", actionDir) } secret := p.CallbackSecret() cfg := p.BuildFail2banActionConfig(callbackURL, serverID, secret) @@ -96,7 +96,7 @@ func EnsureLocalConnectorArtifacts(callbackURL, serverID, configPath string) err jailPath := JailLocal(configPath) if _, err := os.Stat(filepath.Dir(jailPath)); os.IsNotExist(err) { rootDir := NormalizeConfigPath(configPath) - return fmt.Errorf("fail2ban configuration directory does not exist at %s — install fail2ban or set the correct configuration path for this server", rootDir) + return fmt.Errorf("fail2ban configuration directory does not exist at %s - install fail2ban or set the correct configuration path for this server", rootDir) } actionDir := ActionDir(configPath) if err := ensureWritableDirectory(actionDir, "fail2ban action.d directory"); err != nil { diff --git a/internal/fail2ban/manager.go b/internal/fail2ban/manager.go index 3618d4e..0c14c0b 100644 --- a/internal/fail2ban/manager.go +++ b/internal/fail2ban/manager.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -293,7 +293,7 @@ func newConnectorForServer(server shared.Fail2banServer) (Connector, error) { switch server.Type { case "local": if isJailAutoMigrationEnabled() { - debugf("JAIL_AUTOMIGRATION=true: running experimental jail.local → jail.d/ migration for local server %s", server.Name) + debugf("JAIL_AUTOMIGRATION=true: running experimental jail.local -> jail.d/ migration for local server %s", server.Name) if err := MigrateJailsFromJailLocal(server.ConfigPath); err != nil { return nil, fmt.Errorf("failed to initialise local fail2ban connector for %s: %w", server.Name, err) } diff --git a/internal/fail2ban/paths.go b/internal/fail2ban/paths.go index 815777d..e8de2af 100644 --- a/internal/fail2ban/paths.go +++ b/internal/fail2ban/paths.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/fail2ban/paths_test.go b/internal/fail2ban/paths_test.go index 9a372ab..9994a72 100644 --- a/internal/fail2ban/paths_test.go +++ b/internal/fail2ban/paths_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/fail2ban/provider.go b/internal/fail2ban/provider.go index fbbd792..0c25a04 100644 --- a/internal/fail2ban/provider.go +++ b/internal/fail2ban/provider.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/fail2ban/variable_resolver.go b/internal/fail2ban/variable_resolver.go index ea2692c..1a8f7b9 100644 --- a/internal/fail2ban/variable_resolver.go +++ b/internal/fail2ban/variable_resolver.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/fail2ban/variable_resolver_test.go b/internal/fail2ban/variable_resolver_test.go index 3a78726..c0131dc 100644 --- a/internal/fail2ban/variable_resolver_test.go +++ b/internal/fail2ban/variable_resolver_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/integrations/mikrotik.go b/internal/integrations/mikrotik.go index c14eaab..0ef03dd 100644 --- a/internal/integrations/mikrotik.go +++ b/internal/integrations/mikrotik.go @@ -1,3 +1,19 @@ +// Fail2ban UI - A Swiss made, management interface for Fail2ban. +// +// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) +// +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.gnu.org/licenses/agpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package integrations import ( diff --git a/internal/integrations/opnsense.go b/internal/integrations/opnsense.go index d924e34..43011d3 100644 --- a/internal/integrations/opnsense.go +++ b/internal/integrations/opnsense.go @@ -1,3 +1,19 @@ +// Fail2ban UI - A Swiss made, management interface for Fail2ban. +// +// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) +// +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.gnu.org/licenses/agpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package integrations import ( diff --git a/internal/integrations/pfsense.go b/internal/integrations/pfsense.go index 92af0ae..ae9c724 100644 --- a/internal/integrations/pfsense.go +++ b/internal/integrations/pfsense.go @@ -1,3 +1,19 @@ +// Fail2ban UI - A Swiss made, management interface for Fail2ban. +// +// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) +// +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.gnu.org/licenses/agpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package integrations import ( diff --git a/internal/integrations/types.go b/internal/integrations/types.go index e13f8b0..527e9fa 100644 --- a/internal/integrations/types.go +++ b/internal/integrations/types.go @@ -1,3 +1,19 @@ +// Fail2ban UI - A Swiss made, management interface for Fail2ban. +// +// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) +// +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.gnu.org/licenses/agpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package integrations import ( @@ -131,20 +147,3 @@ func Get(id string) (Integration, bool) { return integration, ok } -// Returns the integration or panics. -func MustGet(id string) Integration { - integration, ok := Get(id) - if !ok { - panic(fmt.Sprintf("integration %s not registered", id)) - } - return integration -} - -// Returns all registered integration ids. -func Supported() []string { - keys := make([]string, 0, len(registry)) - for id := range registry { - keys = append(keys, id) - } - return keys -} diff --git a/internal/integrations/types_test.go b/internal/integrations/types_test.go index cee6b32..227d102 100644 --- a/internal/integrations/types_test.go +++ b/internal/integrations/types_test.go @@ -1,3 +1,19 @@ +// Fail2ban UI - A Swiss made, management interface for Fail2ban. +// +// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) +// +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.gnu.org/licenses/agpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package integrations import "testing" diff --git a/internal/shared/server.go b/internal/shared/server.go index b5f8df0..116e586 100644 --- a/internal/shared/server.go +++ b/internal/shared/server.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/shared/validation.go b/internal/shared/validation.go index b48af42..33c2144 100644 --- a/internal/shared/validation.go +++ b/internal/shared/validation.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 50fc3b6..1fc31da 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -596,14 +596,6 @@ INSERT INTO servers ( return err } -func DeleteServer(ctx context.Context, id string) error { - if db == nil { - return errors.New("storage not initialised") - } - _, err := db.ExecContext(ctx, `DELETE FROM servers WHERE id = ?`, id) - return err -} - // ========================================================================= // Ban Events Records // ========================================================================= @@ -1220,6 +1212,7 @@ CREATE INDEX IF NOT EXISTS idx_ban_events_occurred_at ON ban_events(occurred_at) CREATE INDEX IF NOT EXISTS idx_ban_events_ip ON ban_events(ip); CREATE INDEX IF NOT EXISTS idx_ban_events_server_jail_occurred_at ON ban_events(server_id, jail, occurred_at); CREATE INDEX IF NOT EXISTS idx_ban_events_occurred_at_ip ON ban_events(occurred_at, ip, country, event_type, server_id); +CREATE INDEX IF NOT EXISTS idx_ban_events_server_occurred_at ON ban_events(server_id, occurred_at); CREATE TABLE IF NOT EXISTS permanent_blocks ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index 9f05fc7..784b5d7 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/internal/version/version.go b/internal/version/version.go index 770eb8e..c88608d 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/web/advanced_actions.go b/pkg/web/advanced_actions.go index d5ec814..dcbb2a2 100644 --- a/pkg/web/advanced_actions.go +++ b/pkg/web/advanced_actions.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -42,7 +42,7 @@ func evaluateAdvancedActions(ctx context.Context, settings config.AppSettings, s count, err := storage.CountBanEventsByIP(ctx, ip, server.ID) if err != nil { - log.Printf("⚠️ Failed to count ban events for %s: %v", ip, err) + log.Printf("WARNING: Failed to count ban events for %s: %v", ip, err) return } if int(count) < cfg.Threshold { @@ -51,7 +51,7 @@ func evaluateAdvancedActions(ctx context.Context, settings config.AppSettings, s active, err := storage.IsPermanentBlockActive(ctx, ip, cfg.Integration) if err != nil { - log.Printf("⚠️ Failed to check permanent block for %s: %v", ip, err) + log.Printf("WARNING: Failed to check permanent block for %s: %v", ip, err) return } if active { @@ -64,7 +64,7 @@ func evaluateAdvancedActions(ctx context.Context, settings config.AppSettings, s "count": count, "threshold": cfg.Threshold, }, false); err != nil { - log.Printf("⚠️ Failed to permanently block %s: %v", ip, err) + log.Printf("WARNING: Failed to permanently block %s: %v", ip, err) } } @@ -132,7 +132,7 @@ func runAdvancedIntegrationAction(ctx context.Context, action, ip string, settin Details: string(detailsBytes), } if err2 := storage.UpsertPermanentBlock(ctx, rec); err2 != nil { - log.Printf("⚠️ Failed to record permanent block entry: %v", err2) + log.Printf("WARNING: Failed to record permanent block entry: %v", err2) } } diff --git a/pkg/web/assets.go b/pkg/web/assets.go index 18e48be..fac6100 100644 --- a/pkg/web/assets.go +++ b/pkg/web/assets.go @@ -2,11 +2,11 @@ // // Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/web/assets_test.go b/pkg/web/assets_test.go index f7076ef..8403ff4 100644 --- a/pkg/web/assets_test.go +++ b/pkg/web/assets_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/web/auth.go b/pkg/web/auth.go index e6b8416..b6cf21d 100644 --- a/pkg/web/auth.go +++ b/pkg/web/auth.go @@ -2,11 +2,11 @@ // // Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -24,8 +24,6 @@ import ( "github.com/swissmakers/fail2ban-ui/internal/auth" ) -// If OIDC is enabled, this validates the session and redirects to login if not authenticated -// If OIDC is disabled, it allows all requests func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { if !auth.IsEnabled() { @@ -60,7 +58,6 @@ func AuthMiddleware() gin.HandlerFunc { } } -// Checks if path is a public route (that does not require authentication) func isPublicRoute(path string) bool { publicRoutes := []string{ "/auth/login", @@ -81,7 +78,6 @@ func isPublicRoute(path string) bool { return false } -// Checks if the request is an API request func isAPIRequest(c *gin.Context) bool { accept := c.GetHeader("Accept") return strings.Contains(accept, "application/json") || strings.HasPrefix(c.Request.URL.Path, "/api/") diff --git a/pkg/web/authorization.go b/pkg/web/authorization.go index dad8ef2..187412c 100644 --- a/pkg/web/authorization.go +++ b/pkg/web/authorization.go @@ -1,3 +1,19 @@ +// Fail2ban UI - A Swiss made, management interface for Fail2ban. +// +// Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) +// +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.gnu.org/licenses/agpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package web import ( diff --git a/pkg/web/basepath.go b/pkg/web/basepath.go index fe380f2..c1652d0 100644 --- a/pkg/web/basepath.go +++ b/pkg/web/basepath.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -96,7 +96,6 @@ func ExternalPath(internal string) string { } // Wraps an HTTP handler and strips BasePath before route matching. -// resolve against existing /static/* routes. func StripBasePathHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { base := BasePath() diff --git a/pkg/web/basepath_test.go b/pkg/web/basepath_test.go index 06d87e9..a2ef860 100644 --- a/pkg/web/basepath_test.go +++ b/pkg/web/basepath_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/web/callback_secret.go b/pkg/web/callback_secret.go index a828693..c4c6a74 100644 --- a/pkg/web/callback_secret.go +++ b/pkg/web/callback_secret.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -53,15 +53,15 @@ func validateCallbackSecret(c *gin.Context) bool { settings := config.GetSettings() switch classifyCallbackSecret(c.GetHeader("X-Callback-Secret"), settings.CallbackSecret) { case callbackSecretNotConfigured: - log.Printf("⚠️ Callback secret not configured, rejecting request from %s", c.ClientIP()) + log.Printf("WARNING: Callback secret not configured, rejecting request from %s", c.ClientIP()) c.JSON(http.StatusUnauthorized, gin.H{"error": "Callback secret not configured"}) return false case callbackSecretMissingHeader: - log.Printf("⚠️ Missing X-Callback-Secret header in request from %s", c.ClientIP()) + log.Printf("WARNING: Missing X-Callback-Secret header in request from %s", c.ClientIP()) c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing X-Callback-Secret header"}) return false case callbackSecretMismatch: - log.Printf("⚠️ Invalid callback secret in request from %s", c.ClientIP()) + log.Printf("WARNING: Invalid callback secret in request from %s", c.ClientIP()) c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid callback secret"}) return false default: diff --git a/pkg/web/callback_secret_test.go b/pkg/web/callback_secret_test.go index a5c3d4c..08d02bd 100644 --- a/pkg/web/callback_secret_test.go +++ b/pkg/web/callback_secret_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/web/console_logger.go b/pkg/web/console_logger.go index 0f57875..f4829f2 100644 --- a/pkg/web/console_logger.go +++ b/pkg/web/console_logger.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/web/email_header_test.go b/pkg/web/email_header_test.go index 27ea471..ae72876 100644 --- a/pkg/web/email_header_test.go +++ b/pkg/web/email_header_test.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index 7d28e53..9d55908 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -2,11 +2,11 @@ // // Copyright (C) 2026 Swissmakers GmbH (https://swissmakers.ch) // -// Licensed under the GNU General Public License, Version 3 (GPL-3.0) +// Licensed under the GNU Affero General Public License, Version 3 (AGPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// https://www.gnu.org/licenses/gpl-3.0.en.html +// https://www.gnu.org/licenses/agpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -250,7 +250,7 @@ func SummaryHandler(c *gin.Context) { if exists && !hasUI { resp.JailLocalWarning = true } else if !exists { - // File was removed (user finished migration) – initialize a fresh managed file + // File was removed (user finished migration) - initialize a fresh managed file if err := conn.EnsureJailLocalStructure(c.Request.Context()); err != nil { config.DebugLog("Warning: failed to initialize jail.local on summary request: %v", err) } else { @@ -510,22 +510,21 @@ func BanNotificationHandler(c *gin.Context) { var verr validator.ValidationErrors if errors.As(err, &verr) { for _, fe := range verr { - log.Printf("❌ Validation error: Field '%s' violated rule '%s'", fe.Field(), fe.ActualTag()) + log.Printf("ERROR: Validation error: Field '%s' violated rule '%s'", fe.Field(), fe.ActualTag()) } } else { - log.Printf("❌ JSON parsing error -> Action will not be recorded! Details: %v", err) + log.Printf("ERROR: JSON parsing error -> Action will not be recorded! Details: %v", err) } config.DebugLog("Raw JSON that failed to parse: %s", string(body)) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) return } - // Logs the parsed request log.Printf("Parsed ban request successfully - IP: %s, Jail: %s, Hostname: %s, Failures: %s", request.IP, request.Jail, request.Hostname, request.Failures) if err := integrations.ValidateIP(request.IP); err != nil { - log.Printf("⚠️ Invalid IP in ban notification: %s", request.IP) + log.Printf("WARNING: Invalid IP in ban notification: %s", request.IP) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid IP: " + err.Error()}) return } @@ -537,7 +536,7 @@ func BanNotificationHandler(c *gin.Context) { } if err := HandleBanNotification(c.Request.Context(), server, request.IP, request.Jail, request.Hostname, request.Failures, request.Whois, request.Logs); err != nil { - log.Printf("❌ Failed to process ban notification: %v\n", err) + log.Printf("ERROR: Failed to process ban notification: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process ban notification: " + err.Error()}) return } @@ -567,10 +566,10 @@ func UnbanNotificationHandler(c *gin.Context) { var verr validator.ValidationErrors if errors.As(err, &verr) { for _, fe := range verr { - log.Printf("❌ Validation error: Field '%s' violated rule '%s'", fe.Field(), fe.ActualTag()) + log.Printf("ERROR: Validation error: Field '%s' violated rule '%s'", fe.Field(), fe.ActualTag()) } } else { - log.Printf("❌ JSON parsing error -> Action will not be recorded! Details: %v", err) + log.Printf("ERROR: JSON parsing error -> Action will not be recorded! Details: %v", err) } log.Printf("Raw JSON: %s", string(body)) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) @@ -581,7 +580,7 @@ func UnbanNotificationHandler(c *gin.Context) { request.IP, request.Jail, request.Hostname) if err := integrations.ValidateIP(request.IP); err != nil { - log.Printf("⚠️ Invalid IP in unban notification: %s", request.IP) + log.Printf("WARNING: Invalid IP in unban notification: %s", request.IP) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid IP: " + err.Error()}) return } @@ -593,7 +592,7 @@ func UnbanNotificationHandler(c *gin.Context) { } if err := HandleUnbanNotification(c.Request.Context(), server, request.IP, request.Jail, request.Hostname, "", ""); err != nil { - log.Printf("❌ Failed to process unban notification: %v\n", err) + log.Printf("ERROR: Failed to process unban notification: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to process unban notification: " + err.Error()}) return } @@ -1112,7 +1111,7 @@ func UpsertServerHandler(c *gin.Context) { // Checks the integrity AFTER ensuring structure so fresh servers don't trigger a false-positive warning. if exists, hasUI, chkErr := conn.CheckJailLocalIntegrity(c.Request.Context()); chkErr == nil && exists && !hasUI { jailLocalWarning = true - log.Printf("⚠️ Server %s: jail.local is not managed by Fail2ban-UI. Please migrate your jail.local manually (see documentation).", server.Name) + log.Printf("WARNING: Server %s: jail.local is not managed by Fail2ban-UI. Please migrate your jail.local manually (see documentation).", server.Name) } // Tries to restart Fail2ban and performs a basic health check after the server was enabled @@ -1323,7 +1322,7 @@ func resolveWhoisAndCountry(ip, providedWhois, providedCountry string, settings var err error whoisData, err = lookupWhois(ip) if err != nil { - log.Printf("⚠️ Whois lookup failed for IP %s: %v", ip, err) + log.Printf("WARNING: Whois lookup failed for IP %s: %v", ip, err) whoisData = "" } } else { @@ -1336,7 +1335,7 @@ func resolveWhoisAndCountry(ip, providedWhois, providedCountry string, settings var err error country, err = lookupCountry(ip, settings.GeoIPProvider, settings.GeoIPDatabasePath) if err != nil { - log.Printf("⚠️ GeoIP lookup failed for IP %s: %v", ip, err) + log.Printf("WARNING: GeoIP lookup failed for IP %s: %v", ip, err) if whoisData != "" { country = extractCountryFromWhois(whoisData) if country != "" { @@ -1366,7 +1365,7 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip OccurredAt: time.Now().UTC(), } if err := storage.RecordBanEvent(ctx, event); err != nil { - log.Printf("⚠️ Failed to record ban event: %v", err) + log.Printf("WARNING: Failed to record ban event: %v", err) } // Broadcasts the ban event to WebSocket clients @@ -1382,12 +1381,12 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip } if !shouldAlertForCountry(country, settings.AlertCountries) { - log.Printf("❌ IP %s belongs to %s, which is NOT in alert countries (%v). No alert sent.", ip, displayCountry, settings.AlertCountries) + log.Printf("ERROR: IP %s belongs to %s, which is NOT in alert countries (%v). No alert sent.", ip, displayCountry, settings.AlertCountries) return nil } if !settings.EmailAlertsForBans { - log.Printf("❌ Alerts for bans are disabled. No alert sent for IP %s", ip) + log.Printf("ERROR: Alerts for bans are disabled. No alert sent for IP %s", ip) return nil } @@ -1397,7 +1396,6 @@ func HandleBanNotification(ctx context.Context, server config.Fail2banServer, ip // Records an unban event, broadcasts it via WebSocket, and sends an email alert if enabled. func HandleUnbanNotification(ctx context.Context, server config.Fail2banServer, ip, jail, hostname, whois, country string) error { - // Loads the settings to get alert countries and GeoIP provider from the database settings := config.GetSettings() whoisData, country := resolveWhoisAndCountry(ip, whois, country, settings) event := storage.BanEventRecord{ @@ -1414,7 +1412,7 @@ func HandleUnbanNotification(ctx context.Context, server config.Fail2banServer, OccurredAt: time.Now().UTC(), } if err := storage.RecordBanEvent(ctx, event); err != nil { - log.Printf("⚠️ Failed to record unban event: %v", err) + log.Printf("WARNING: Failed to record unban event: %v", err) } // Broadcasts the unban event to WebSocket clients @@ -1449,7 +1447,7 @@ func HandleUnbanNotification(ctx context.Context, server config.Fail2banServer, func dispatchAlertAsync(alertType, ip, jail, hostname, failures, whois, logs, country string, settings config.AppSettings) { go func() { if err := dispatchAlert(alertType, ip, jail, hostname, failures, whois, logs, country, settings); err != nil { - log.Printf("❌ Failed to send %s alert for IP %s: %v", alertType, ip, err) + log.Printf("ERROR: Failed to send %s alert for IP %s: %v", alertType, ip, err) if wsHub != nil { wsHub.BroadcastToast("error", fmt.Sprintf("Failed to send %s alert for %s: %v", alertType, ip, err)) } @@ -1857,7 +1855,6 @@ func renderIndexPage(c *gin.Context) { autoDark := os.Getenv("AUTODARK") == "true" || os.Getenv("AUTODARK") == "1" languageOptions := listLocaleOptions() - // Checks if OIDC is enabled and skip login page setting oidcEnabled := auth.IsEnabled() skipLoginPage := false if oidcEnabled { @@ -1867,7 +1864,6 @@ func renderIndexPage(c *gin.Context) { } } - // Checks is a user wants to disable the github versioning check updateCheckEnabled := os.Getenv("UPDATE_CHECK") != "false" urlPrefix := BasePath() @@ -2151,7 +2147,6 @@ func SetJailFilterConfigHandler(c *gin.Context) { config.DebugLog("Found original filter directive in jail config: %s", originalFilterName) } - // Extracts the new filter name from the updated jail config newFilterName := fail2ban.ExtractFilterFromJailConfig(req.Jail) if newFilterName == "" { newFilterName = jail @@ -2188,14 +2183,13 @@ func SetJailFilterConfigHandler(c *gin.Context) { config.DebugLog("No jail config provided, skipping") } - // Reloads Fail2ban config.DebugLog("Reloading fail2ban") if err := conn.Reload(c.Request.Context()); err != nil { - log.Printf("⚠️ Config saved but fail2ban reload failed: %v", err) + log.Printf("WARNING: Config saved but fail2ban reload failed: %v", err) // If reload fails, we automatically disable the jail so Fail2ban won't crash on next restart (invalid filter/jail config) disableUpdate := map[string]bool{jail: false} if disableErr := conn.UpdateJailEnabledStates(c.Request.Context(), disableUpdate); disableErr != nil { - log.Printf("⚠️ Failed to auto-disable jail %s after reload failure: %v", jail, disableErr) + log.Printf("WARNING: Failed to auto-disable jail %s after reload failure: %v", jail, disableErr) c.JSON(http.StatusOK, gin.H{ "message": "Config saved successfully, but fail2ban reload failed", "warning": err.Error(), @@ -2203,7 +2197,7 @@ func SetJailFilterConfigHandler(c *gin.Context) { return } if reloadErr2 := conn.Reload(c.Request.Context()); reloadErr2 != nil { - log.Printf("⚠️ Failed to reload fail2ban after auto-disabling jail %s: %v", jail, reloadErr2) + log.Printf("WARNING: Failed to reload fail2ban after auto-disabling jail %s: %v", jail, reloadErr2) } c.JSON(http.StatusOK, gin.H{ "message": "Config saved successfully, but fail2ban reload failed", @@ -2246,12 +2240,10 @@ func TestLogpathHandler(c *gin.Context) { var originalLogpath string - // Checks if a logpath is provided in the request body var reqBody struct { Logpath string `json:"logpath"` } if err := c.ShouldBindJSON(&reqBody); err == nil && reqBody.Logpath != "" { - // Uses the logpath from the request body (from textarea) originalLogpath = strings.TrimSpace(reqBody.Logpath) config.DebugLog("Using logpath from request body: %s", originalLogpath) } else { @@ -2262,7 +2254,6 @@ func TestLogpathHandler(c *gin.Context) { return } - // Extracts the logpath from the jail config originalLogpath = fail2ban.ExtractLogpathFromJailConfig(jailCfg) if originalLogpath == "" { c.JSON(http.StatusOK, gin.H{ @@ -2281,7 +2272,6 @@ func TestLogpathHandler(c *gin.Context) { return } - // Gets the server type for response metadata server := conn.Server() isLocalServer := server.Type == "local" @@ -2463,7 +2453,6 @@ func AdvancedActionsTestHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Action %s completed for %s", action, req.IP)}) } -// Returns a sorted slice of jail names from the map. func getJailNames(jails map[string]bool) []string { names := make([]string, 0, len(jails)) for name := range jails { @@ -2497,7 +2486,6 @@ func splitLogpaths(raw string) []string { var jailErrorPattern = regexp.MustCompile(`Errors in jail '([^']+)'`) -// Extracts problematic jail names from Fail2ban reload output. func parseJailErrorsFromReloadOutput(output string) []string { var problematicJails []string lines := strings.Split(output, "\n") @@ -2625,14 +2613,12 @@ func UpdateJailManagementHandler(c *gin.Context) { // Reloads fail2ban to apply the changes reloadErr := conn.Reload(c.Request.Context()) - // Checks for errors in reload output var problematicJails []string var detailedErrorOutput string if reloadErr != nil { errMsg := reloadErr.Error() config.DebugLog("Error: failed to reload fail2ban after updating jail settings: %v", reloadErr) - // Extracts the output from the error message if strings.Contains(errMsg, "(output:") { outputStart := strings.Index(errMsg, "(output:") + 8 outputEnd := strings.LastIndex(errMsg, ")") @@ -3063,7 +3049,7 @@ func applySettingsUpdate(c *gin.Context, req config.AppSettings) { config.DebugLog("Updating DEFAULT settings on server: %s (type: %s)", server.Name, server.Type) if err := conn.UpdateDefaultSettings(c.Request.Context()); err != nil { errorMsg := fmt.Sprintf("Failed to update DEFAULT settings on %s: %v", server.Name, err) - log.Printf("⚠️ %s", errorMsg) + log.Printf("WARNING: %s", errorMsg) updateErrors = append(updateErrors, errorMsg) } else { config.DebugLog("Successfully updated DEFAULT settings on %s", server.Name) @@ -3233,7 +3219,6 @@ func CreateFilterHandler(c *gin.Context) { req.Content = fmt.Sprintf("# Filter: %s\n", req.FilterName) } - // Create the filter if err := conn.CreateFilter(c.Request.Context(), req.FilterName, req.Content); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create filter: " + err.Error()}) return @@ -3411,7 +3396,7 @@ func sanitizeEmailHeader(value string) string { func sendEmail(to, subject, body string, settings config.AppSettings) error { recipients := shared.SplitCommaList(to) if len(recipients) == 0 { - log.Printf("⚠️ sendEmail skipped: no recipients provided.") + log.Printf("WARNING: sendEmail skipped: no recipients provided.") return nil } @@ -3424,20 +3409,20 @@ func sendEmail(to, subject, body string, settings config.AppSettings) error { } } if allPlaceholder { - log.Printf("⚠️ sendEmail skipped: all recipients are still the default placeholder (alerts@example.com). Please update the 'Destination Email' in Settings → Alert Settings.") + log.Printf("WARNING: sendEmail skipped: all recipients are still the default placeholder (alerts@example.com). Please update the 'Destination Email' in Settings -> Alert Settings.") return nil } needsAuth := settings.SMTP.AuthMethod != "none" if settings.SMTP.Host == "" || settings.SMTP.From == "" || (needsAuth && (settings.SMTP.Username == "" || settings.SMTP.Password == "")) { err := errors.New("SMTP settings are incomplete. Please configure all required fields") - log.Printf("❌ sendEmail validation failed: %v (Host: %q, Username: %q, From: %q)", err, settings.SMTP.Host, settings.SMTP.Username, settings.SMTP.From) + log.Printf("ERROR: sendEmail validation failed: %v (Host: %q, Username: %q, From: %q)", err, settings.SMTP.Host, settings.SMTP.Username, settings.SMTP.From) return err } if settings.SMTP.Port <= 0 || settings.SMTP.Port > 65535 { err := errors.New("SMTP port must be between 1 and 65535") - log.Printf("❌ sendEmail validation failed: %v (Port: %d)", err, settings.SMTP.Port) + log.Printf("ERROR: sendEmail validation failed: %v (Port: %d)", err, settings.SMTP.Port) return err } @@ -3471,10 +3456,10 @@ func sendEmail(to, subject, body string, settings config.AppSettings) error { } auth, err := getSMTPAuth(settings.SMTP.Username, settings.SMTP.Password, authMethod, smtpHost) if err != nil { - log.Printf("❌ sendEmail: failed to create SMTP auth (method: %q): %v", authMethod, err) + log.Printf("ERROR: sendEmail: failed to create SMTP auth (method: %q): %v", authMethod, err) return fmt.Errorf("failed to create SMTP auth: %w", err) } - log.Printf("📧 sendEmail: Using SMTP auth method: %q, host: %s, port: %d, useTLS: %v, insecureSkipVerify: %v", authMethod, smtpHost, smtpPort, settings.SMTP.UseTLS, settings.SMTP.InsecureSkipVerify) + log.Printf("sendEmail: Using SMTP auth method: %q, host: %s, port: %d, useTLS: %v, insecureSkipVerify: %v", authMethod, smtpHost, smtpPort, settings.SMTP.UseTLS, settings.SMTP.InsecureSkipVerify) // Port 465 uses implicit TLS (SMTPS); all other ports use plain SMTP with optional STARTTLS. useImplicitTLS, useSTARTTLS := smtpTLSMode(smtpPort, settings.SMTP.UseTLS) @@ -3519,18 +3504,18 @@ func sendEmail(to, subject, body string, settings config.AppSettings) error { if auth != nil { if err := client.Auth(auth); err != nil { - log.Printf("❌ sendEmail: SMTP authentication failed: %v", err) + log.Printf("ERROR: sendEmail: SMTP authentication failed: %v", err) return fmt.Errorf("SMTP authentication failed: %w", err) } - log.Printf("📧 sendEmail: SMTP authentication successful") + log.Printf("sendEmail: SMTP authentication successful") } err = sendSMTPMessage(client, settings.SMTP.From, recipients, msg) if err != nil { - log.Printf("❌ sendEmail: Failed to send message: %v", err) + log.Printf("ERROR: sendEmail: Failed to send message: %v", err) return err } - log.Printf("📧 sendEmail: Successfully sent email to %s", strings.Join(recipients, ", ")) + log.Printf("sendEmail: Successfully sent email to %s", strings.Join(recipients, ", ")) return nil } @@ -3620,16 +3605,16 @@ func buildClassicEmailBody(title, intro string, details []emailDetail, whoisHTML
Swissmakers GmbH -

🚨 %s

+

ALERT: %s

%s

%s
-

🔍 %s

+

%s

%s -

📄 %s

+

%s

%s
' + '
' - + ' ' + escapeHtml(server) + ' • ' + escapeHtml(country) + + ' ' + escapeHtml(server) + ' - ' + escapeHtml(country) + '
' + '
' + '