From 68e416c54722e5afd2941db56257f0c40965f9d6 Mon Sep 17 00:00:00 2001 From: Ismo Vuorinen Date: Tue, 27 Aug 2024 22:06:33 +0300 Subject: [PATCH] feat: first version of the script --- .github/README.md | 66 ++ .github/labels.yml | 164 +++++ .github/workflows/pr-lint.yml | 2 +- .github/workflows/release-drafter.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/sync-labels.yml | 2 +- f2b | 848 ++++++++++++++++++++++++++ 7 files changed, 1082 insertions(+), 4 deletions(-) create mode 100644 .github/README.md create mode 100644 .github/labels.yml create mode 100755 f2b diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..b2177c3 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,66 @@ +# ivuorinen/f2b + +A fail2ban wrapper for easier management and listing of banned IP's in your jails. + +Requires fail2ban to be installed and running. Should work on most Linux distributions. +Developed against `fail2ban` version 0.11.2 on Ubuntu 22.04.4 LTS using nvim. + +[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) ![GitHub file size in bytes](https://img.shields.io/github/size/ivuorinen/f2b/f2b) + +## Installation + +```bash +curl https://raw.githubusercontent.com/ivuorinen/f2b/main/f2b > f2b +chmod +x f2b +./f2b version +``` + +Requiements: `fail2ban` (duh), and few other default tools. +`awk`, `cat`, `date`, `grep`, `ls`, `sed`, `sort`, `tail`, `tr`, `wc`, and `zcat` should be installed. +Those are usually installed by default on most Linux distributions. The script will tell you if something is missing. + +If running commands straight from the internet scares you (as it should) you can +open the f2b script in your favourite editor (or here in GitHub) and view the source. + +I promise I'm not doing anything weird in the script. + +## Usage + +It uses several fail2ban commands to get the information it needs, so it needs to be run as root. + +```bash +Usage: f2b [command] [options] + list-jails List all jails + status all Show status of all jails + status [jail] Show status of a specific jail + banned Show all banned IP addresses with ban time left + banned [jail] Show all banned IP addresses with ban time left in a jail + ban [ip] Ban IP address in all jails + ban [ip] [jail] Ban IP address in a specific jail + unban [ip] Unban IP address in all jails + unban [ip] [jail] Unban IP address in a specific jail + test [ip] Test if IP address is banned + logs Show fail2ban logs + logs all [ip] Show logs for a specific IP address in all jails + logs [jail] Show logs for a specific jail + logs [jail] [ip] Show logs for a specific jail and IP address + logs-watch Watch fail2ban logs + logs-watch all [ip] Watch logs for a specific IP address + logs-watch [jail] Watch logs for a specific jail + logs-watch [jail] [ip] Watch logs for a specific jail and IP address + test-filter [filter] Test a fail2ban filter + service start Start fail2ban + service stop Stop fail2ban + service restart Restart fail2ban + help Show help + version Show version +``` + +## Authors + +- [@ivuorinen](https://github.com/ivuorinen) + +## License + +[MIT](https://choosealicense.com/licenses/mit/) + diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..170ca49 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,164 @@ +--- +- name: "breaking-change" + color: ee0701 + description: "A breaking change for existing users." +- name: "bugfix" + color: ee0701 + description: "Inconsistencies or issues which will cause a problem for users or implementors." +- name: "documentation" + color: 0052cc + description: "Solely about the documentation of the project." +- name: "enhancement" + color: 1d76db + description: "Enhancement of the code, not introducing new features." +- name: "refactor" + color: 1d76db + description: "Improvement of existing code, not introducing new features." +- name: "performance" + color: 1d76db + description: "Improving performance, not introducing new features." +- name: "new-feature" + color: 0e8a16 + description: "New features or options." +- name: "maintenance" + color: 2af79e + description: "Generic maintenance tasks." +- name: "ci" + color: 1d76db + description: "Work that improves the continue integration." +- name: "dependencies" + color: 1d76db + description: "Upgrade or downgrade of project dependencies." +- name: "translations" + color: d4c5f9 + description: "Impacts translations." + +- name: "in-progress" + color: fbca04 + description: "Issue is currently being resolved by a developer." +- name: "stale" + color: fef2c0 + description: "There has not been activity on this issue or PR for quite some time." +- name: "no-stale" + color: fef2c0 + description: "This issue or PR is exempted from the stable bot." + +- name: "security" + color: ee0701 + description: "Marks a security issue that needs to be resolved asap." +- name: "incomplete" + color: fef2c0 + description: "Marks a PR or issue that is missing information." +- name: "invalid" + color: fef2c0 + description: "Marks a PR or issue that is missing information." + +- name: "beginner-friendly" + color: 0e8a16 + description: "Good first issue for people wanting to contribute to the project." +- name: "help-wanted" + color: 0e8a16 + description: "We need some extra helping hands or expertise in order to resolve this." + +- name: "hacktoberfest" + description: "Issues/PRs are participating in the Hacktoberfest." + color: fbca04 +- name: "hacktoberfest-accepted" + description: "Issues/PRs are participating in the Hacktoberfest." + color: fbca04 + +- name: "priority-critical" + color: ee0701 + description: "This should be dealt with ASAP. Not fixing this issue would be a serious error." +- name: "priority-high" + color: b60205 + description: "After critical issues are fixed, these should be dealt with before any further issues." +- name: "priority-medium" + color: 0e8a16 + description: "This issue may be useful, and needs some attention." +- name: "priority-low" + color: e4ea8a + description: "Nice addition, maybe... someday..." + +- name: "major" + color: b60205 + description: "This PR causes a major version bump in the version number." +- name: "minor" + color: 0e8a16 + description: "This PR causes a minor version bump in the version number." + +# Areas +- name: area/ansible + color: "0e8a16" +- name: area/docs + color: "0e8a16" +- name: area/github + color: "0e8a16" +- name: area/kubernetes + color: "0e8a16" +- name: area/taskfile + color: "0e8a16" +- name: area/terraform + color: "0e8a16" +# Clusters +- name: cluster/main + color: "ffc300" +- name: cluster/storage + color: "ffc300" +# Renovate Types +- name: renovate/ansible + color: "027fa0" +- name: renovate/container + color: "027fa0" +- name: renovate/github-action + color: "027fa0" +- name: renovate/grafana-dashboard + color: "027fa0" +- name: renovate/github-release + color: "027fa0" +- name: renovate/helm + color: "027fa0" +- name: renovate/terraform + color: "027fa0" +# Semantic Types +- name: type/digest + color: "ffeC19" +- name: type/patch + color: "ffeC19" +- name: type/minor + color: "ff9800" +- name: type/major + color: "f6412d" +# Uncategorized +- name: community + color: "370fb2" +- name: hold + color: "ee0701" + +## more info https://github.com/crazy-max/ghaction-github-labeler +- # automerge + name: ":bell: automerge" + color: "8f4fbc" + description: "" +- # bot + name: ":robot: bot" + color: "69cde9" + description: "" +- # bug + name: ":bug: bug" + color: "b60205" + description: "" +- # documentation + name: ":memo: documentation" + color: "c5def5" + description: "" +- # duplicate + name: ":busts_in_silhouette: duplicate" + color: "cccccc" + description: "" +- # enhancement + name: ":sparkles: enhancement" + color: "0054ca" + description: "" +- # feature request + name: ":bulb: feature request" diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index f4f8670..ef94fd0 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -15,4 +15,4 @@ permissions: jobs: SuperLinter: - uses: ivuorinen/.github/.github/workflows/pr-lint.yml@main + uses: ivuorinen/ivuorinen/.github/workflows/pr-lint.yml@main diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 02bb9ce..51db14c 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -11,4 +11,4 @@ permissions: jobs: Draft: - uses: ivuorinen/.github/.github/workflows/sync-labels.yml@main + uses: ivuorinen/ivuorinen/.github/workflows/sync-labels.yml@main diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 112ecb4..e4155a7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,4 +15,4 @@ permissions: jobs: stale: - uses: ivuorinen/.github/.github/workflows/stale.yml@main + uses: ivuorinen/ivuorinen/.github/workflows/stale.yml@main diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index f8dbfba..86abe92 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -18,4 +18,4 @@ permissions: jobs: SyncLabels: - uses: ivuorinen/.github/.github/workflows/sync-labels.yml@main + uses: ivuorinen/ivuorinen/.github/workflows/sync-labels.yml@main diff --git a/f2b b/f2b new file mode 100755 index 0000000..2c381db --- /dev/null +++ b/f2b @@ -0,0 +1,848 @@ +#!/usr/bin/env bash +# +# This script is a wrapper for `fail2ban-client` and allows you to use +# short hand commands to interact with fail2ban. +# Commands include: list jails, status of jails, ban/unban IP addresses, etc. +# +# Author: Ismo Vuorinen (2024) +# License: MIT +# Source: https://github.com/ivuorinen/f2b + +VERSION="1.0.0" # Update version number + +# Get basename for this script +F2B_SCRIPT=$(basename "$0") +# Get path to fail2ban-client +F2B_CLIENT=$(command -v fail2ban-client) + +# Check if fail2ban-client is installed +if [ -z "$F2B_CLIENT" ]; then + echo "Error: fail2ban-client is not installed, or not in the PATH." + exit 1 +fi + +# Check for all the required command line tools this script uses +F2B_REQUIRED_TOOLS="awk cat date grep ls sed sort tail tr wc zcat" +F2B_REQUIRED_TOOLS_AVAILABLE=1 +for TOOL in $F2B_REQUIRED_TOOLS; do + if ! command -v "$TOOL" &>/dev/null; then + echo "Error: \"$TOOL\" is required but not installed." + F2B_REQUIRED_TOOLS_AVAILABLE=0 + fi +done +if [ $F2B_REQUIRED_TOOLS_AVAILABLE -eq 0 ]; then + echo "Please install the required tools and try again." + exit 1 +fi + +# Humans can't remember to run scripts as root, so let's remind them, or run as sudo +# https://stackoverflow.com/a/28776100 +if [ "$(id -u)" != "0" ]; then + # Check that user belongs to sudo group or is sudoer + if groups | grep -q -w sudo; then + F2B_CLIENT="sudo $F2B_CLIENT" + else + echo "Please run this script as root or add yourself to the sudo group." + exit 1 + fi +fi + +# Function to compare version strings +# $1: version string of form 1.2.3 +# Improved from https://stackoverflow.com/a/53400482 +# Usage: (( $(ver 1.2.3) >= $(ver 1.2.4) )) && echo "yes" || echo "no" +ver() { + local SPLIT_VERSION=() + read -r -a SPLIT_VERSION <<<"${1//./ }" + while [ ${#SPLIT_VERSION[@]} -lt 3 ]; do + SPLIT_VERSION+=("0") + done + printf "%02d%02d%02d" "${SPLIT_VERSION[0]}" "${SPLIT_VERSION[1]}" "${SPLIT_VERSION[2]}" +} + +# Check if fail2ban version is 0.11.0 or newer +# The script was developed against fail2ban 0.11.2 +F2B_VER="$($F2B_CLIENT -V)" +F2B_REQ="0.11.0" +if (($(ver "$F2B_VER") < $(ver "$F2B_REQ"))); then + echo "Error: fail2ban version $F2B_REQ or newer is required." + echo " Your version: $F2B_VER" + exit 1 +fi + +# Get arguments and convert to lowercase +F2B_ARG1=$(echo "$1" | tr '[:upper:]' '[:lower:]') +F2B_ARG2=$(echo "$2" | tr '[:upper:]' '[:lower:]') +F2B_ARG3=$(echo "$3" | tr '[:upper:]' '[:lower:]') + +# If there are more than 3 arguments, show error +if [ "$#" -gt 3 ]; then + echo "Error: Too many arguments." + exit 1 +fi + +if [ -z "$F2B_ARG1" ]; then + F2B_ARG1="" +fi +if [ -z "$F2B_ARG2" ]; then + F2B_ARG2="" +fi +if [ -z "$F2B_ARG3" ]; then + F2B_ARG3="" +fi + +# Check if fail2ban is running +if ! $F2B_CLIENT ping &>/dev/null; then + echo "Error: fail2ban is not running." + exit 1 +fi + +# Get list of jails and replace "," with space +F2B_JAILS=$($F2B_CLIENT status | tail -n1 | cut -d':' -f2- | tr -d '[:space:]' | tr ',' ' ') +read -r -a F2B_JAILS_ARRAY <<<"$F2B_JAILS" + +# Return f2b help +# Usage: $0 help +help() { + echo "Usage: $F2B_SCRIPT [command] [options]" + echo " list-jails List all jails" + echo " status all Show status of all jails" + echo " status [jail] Show status of a specific jail" + echo " banned Show all banned IP addresses with ban time left" + echo " banned [jail] Show all banned IP addresses with ban time left in a jail" + echo " ban [ip] Ban IP address in all jails" + echo " ban [ip] [jail] Ban IP address in a specific jail" + echo " unban [ip] Unban IP address in all jails" + echo " unban [ip] [jail] Unban IP address in a specific jail" + echo " test [ip] Test if IP address is banned" + echo " logs Show fail2ban logs" + echo " logs all [ip] Show logs for a specific IP address in all jails" + echo " logs [jail] Show logs for a specific jail" + echo " logs [jail] [ip] Show logs for a specific jail and IP address" + echo " logs-watch Watch fail2ban logs" + echo " logs-watch all [ip] Watch logs for a specific IP address" + echo " logs-watch [jail] Watch logs for a specific jail" + echo " logs-watch [jail] [ip] Watch logs for a specific jail and IP address" + echo " test-filter [filter] Test a fail2ban filter" + echo " service start Start fail2ban" + echo " service stop Stop fail2ban" + echo " service restart Restart fail2ban" + echo " help Show help" + echo " version Show version" +} + +# {{{ + +# Get fail2ban log files and filter by jail and ip if provided +# Usage: f2b_jail_get_log_entries +# Example: f2b_jail_get_log_entries +# Example: f2b_jail_get_log_entries sshd +# Example: f2b_jail_get_log_entries sshd 1.2.3.4 +f2b_jail_get_log_entries() { + local JAIL=${1:-""} # default to empty string if not provided + local IP=${2:-""} # default to empty string if not provided + local LOG_FILES="" + LOG_FILES=$(ls -1 --color=never /var/log/fail2ban.log* 2>/dev/null) + + # If $LOG_FILES is empty, return + if [ -z "$LOG_FILES" ]; then + echo "" + return 0 + fi + + # Loop through log files and get log entries, use cat for normal, + # and zcat for compressed files, concat all log entries into one local string LOG_ENTRIES + local LOG_ENTRIES="" + for LOG_FILE in $LOG_FILES; do + if [ -f "$LOG_FILE" ]; then + if file "$LOG_FILE" | grep -q "compressed"; then + LOG_ENTRIES="$LOG_ENTRIES\n$(zcat "$LOG_FILE")\n" + else + LOG_ENTRIES="$LOG_ENTRIES\n$(cat "$LOG_FILE")\n" + fi + fi + done + + # If $JAIL is not empty, and is not empty string, filter by jail + if [ -n "$JAIL" ] && [ "$JAIL" != "" ]; then + LOG_ENTRIES=$(echo "$LOG_ENTRIES" | grep "[$JAIL]") + fi + + # If $IP is not empty, filter by IP address + if [ -n "$IP" ] && [ "$IP" != "" ]; then + LOG_ENTRIES=$(echo "$LOG_ENTRIES" | grep "$IP") + fi + + # Return log entries + echo "$LOG_ENTRIES" +} + +# Poll fail2ban logs every 5 seconds +# Usage: f2b_poll_jail_log_entries [jail] [ip] +# Example: f2b_poll_jail_log_entries sshd +# Example: f2b_poll_jail_log_entries sshd 1.2.3.4 +f2b_poll_jail_log_entries() { + local JAIL=${1:-""} + local IP=${2:-""} + local LOG_ENTRIES="" + LOG_ENTRIES=$(f2b_jail_get_log_entries "$JAIL" "$IP" | tail -n10) + + echo "$LOG_ENTRIES" + while true; do + NEW_LOG_ENTRIES=$(f2b_jail_get_log_entries "$JAIL" "$IP" | tail -n10) + if [ "$LOG_ENTRIES" != "$NEW_LOG_ENTRIES" ]; then + echo "$NEW_LOG_ENTRIES" + LOG_ENTRIES="$NEW_LOG_ENTRIES" + fi + sleep 5 + done + return 0 +} + +# Test if a fail2ban jail exists, return 0 if exists, 1 if not +# Usage: f2b_jail_exists +# Example: f2b_jail_exists sshd +f2b_jail_exists() { + local JAIL=${1:-""} + + if [ -z "$JAIL" ] && [ "$JAIL" != "" ]; then + echo "[f2b_jail_exists] Error: Please provide a jail to check if it exists." + exit 1 + fi + + local JAILS="" + JAILS=$(echo "$F2B_JAILS" | tr ',' ' ') + for J in $JAILS; do + if [ "$J" == "$JAIL" ]; then + return 0 + fi + done + echo "Error: Jail '$JAIL' does not exist." + echo " Existing jails: $F2B_JAILS" + exit 1 +} + +# Convert seconds to hours, minutes, and seconds +# Usage: f2b_secs_to_hours_minutes_seconds +# Example: f2b_secs_to_hours_minutes_seconds 3600 (01:00:00) +# Example: f2b_secs_to_hours_minutes_seconds 3661 (01:01:01) +# Returns: hours:minutes:seconds +f2b_secs_to_hours_minutes_seconds() { + local SECONDS=${1:-0} + + if [ -z "$SECONDS" ]; then + echo "[f2b_secs_to_hours_minutes_seconds] Error: Please provide seconds to convert." + exit 1 + fi + + if [ "$SECONDS" -lt 0 ]; then + echo "[f2b_secs_to_hours_minutes_seconds] Error: Seconds must be a positive integer." + exit 1 + fi + + local SECONDS=$((SECONDS % 86400)) + local HOURS=$((SECONDS / 3600)) + local SECONDS=$((SECONDS % 3600)) + local MINUTES=$((SECONDS / 60)) + local SECONDS=$((SECONDS % 60)) + + # Pad hours, minutes, and seconds with zeros if they are less than 10 + echo "$(printf "%02d" "$HOURS"):$(printf "%02d" "$MINUTES"):$(printf "%02d" "$SECONDS")" +} + +# Ban IP address in a specific jail, if provided +# Usage: ban_ip [ip] +# Example: ban_ip 1.2.3.4 (to ban in all jails) +# Example: ban_ip 1.2.3.4 sshd (to ban in a specific jail) +f2b_ban_ip() { + local IP=${1:-""} + local JAIL=${2:-""} + + if [ -z "$IP" ] || [ "$IP" == "" ]; then + printf "[f2b_ban_ip] Error: Please provide an IP address to ban.\n" + exit 1 + fi + if [ -z "$JAIL" ] || [ "$JAIL" == "" ]; then + printf "[f2b_ban_ip] Error: Please provide a jail to ban IP address in.\n" + exit 1 + fi + + COMMAND_OUTPUT=$($F2B_CLIENT set "$JAIL" banip "$F2B_ARG2") + if [ "$COMMAND_OUTPUT" -eq "0" ]; then + printf "(!) Banned in %s: %s - Banned\n" "$JAIL" "$F2B_ARG2" + return 0 + fi + if [ "$COMMAND_OUTPUT" -eq "1" ]; then + printf "(!) Banned in %s: %s - Already banned\n" "$JAIL" "$F2B_ARG2" + return 0 + fi + printf "(!) Banned in %s: %s - Unknown error\n" "$JAIL" "$F2B_ARG2" + return 1 +} + +# Unban IP address in a specific jail, if provided +# Usage: f2b_unban_ip [ip] [jail] +# Example: f2b_unban_ip 1.2.3.4 +# Example: f2b_unban_ip 1.2.3.4 sshd +f2b_unban_ip() { + local IP=${1:-""} + local JAIL=${2:-""} + + if [ -z "$IP" ] || [ "$IP" == "" ]; then + printf "[f2b_unban_ip] Error: Please provide an IP address to unban.\n" + exit 1 + fi + if [ -z "$JAIL" ] || [ "$JAIL" == "" ]; then + printf "[f2b_unban_ip] Error: Please provide a jail to unban IP address from.\n" + exit 1 + fi + + COMMAND_OUTPUT=$($F2B_CLIENT set "$JAIL" unbanip "$F2B_ARG2") + if [ "$COMMAND_OUTPUT" -eq "0" ]; then + printf "(!) Unbanned in %s: %s - Unbanned\n" "$JAIL" "$F2B_ARG2" + return 0 + fi + if [ "$COMMAND_OUTPUT" -eq "1" ]; then + printf "(!) Unbanned in %s: %s - Already unbanned\n" "$JAIL" "$F2B_ARG2" + return 0 + fi + printf "(!) Unbanned in %s: %s - Unknown error\n" "$JAIL" "$F2B_ARG2" + return 1 +} + +# Get all banned IPs from all jails in a nice table format +# Usage: f2b_banned_ips [jail] (default: all) +# Example: f2b_banned_ips +# Example: f2b_banned_ips sshd +# Example: f2b_banned_ips all +# Returns: table of banned IPs and some statistics +f2b_banned_ips() { + local JAIL=${1:-"all"} + + # If JAIL is something other than "all", check if the jail exists + # Then set the JAILS_TO_LOOP variable to the jail name, + # otherwise loop through all known jails + if [ "$JAIL" != "all" ]; then + f2b_jail_exists "$JAIL" + JAILS_TO_LOOP="$JAIL" + else + JAILS_TO_LOOP="$F2B_JAILS" + fi + + # Set local variables + local BANNED_IPS="" # List of all banned IPs + local UNIQUE_IPS_LIST="" # List of unique IPs + local UNIQUE_IPS_COUNT=0 # Number of unique IPs + local OLDEST_BAN_DATE=9999999999 # Anything will be older than this + local NEWEST_BAN_DATE=0 # Anything will be newer than this + + # Get all banned ips from all jails using fail2ban-client get banip --with-time + # This is many times faster than grepping the fail2ban log file. + for J in $JAILS_TO_LOOP; do + # The output of fail2ban-client get banip --with-time is: + # [IP Address] [Date and Time Banned] + [Bantime] = [Unban Date and Time] + # we need to add the jail name to the end of the line and format it as: + # [Unban Date and Time]|[Date and Time Banned]|[IP Address]|[Bantime]|[Jail Name] + # and then sort it by the unban date and time so the oldest bans are first + JAILED_IPS=$($F2B_CLIENT get "$J" banip --with-time) + + # If the output is empty, skip to the next jail + if [ -z "$JAILED_IPS" ]; then + continue + fi + + # Take the output of the fail2ban-client command and format it as: + # [Unban Date and Time]|[Date and Time Banned]|[IP Address]|[Bantime]|[Jail Name] + JAILED_IPS=$( + echo "$JAILED_IPS" | + awk -v jail="$J" '{print $7 "T" $8 "|" $2 "T" $3 "|" $1 "|" $5 "|" jail}' + ) + + # Remove any lines that begin with "T" character. + # This happens because we are using the "T" character as + # a separator in the awk command above for the date and time + # and if the date and time are empty, the line will begin with "T" + JAILED_IPS=$(echo "$JAILED_IPS" | grep -v "^T") + + # Again, if filtering JAILED_IPS results in an empty string, skip to the next jail + if [ -z "$JAILED_IPS" ]; then + continue + fi + + # Collect statistics + UNIQUE_IPS_LIST=$(echo "$JAILED_IPS" | awk -F"|" '{print $3}' | sort -u) + UNIQUE_IPS_COUNT=$(echo "$UNIQUE_IPS_LIST" | wc -l) + OLDEST_BAN_DATE_JAIL=$(echo "$JAILED_IPS" | head -n1 | awk -F"|" '{print $2}') + NEWEST_BAN_DATE_JAIL=$(echo "$JAILED_IPS" | tail -n1 | awk -F"|" '{print $2}') + + # Convert the oldest and newest ban dates to a human readable format + # and then to seconds since epoch for comparison + OLDEST_BAN_DATE_JAIL=$(date -d "$OLDEST_BAN_DATE_JAIL" +"%Y-%m-%d %H:%M:%S") + NEWEST_BAN_DATE_JAIL=$(date -d "$NEWEST_BAN_DATE_JAIL" +"%Y-%m-%d %H:%M:%S") + OLDEST_BAN_DATE_SECS=$(date -d "$OLDEST_BAN_DATE_JAIL" +"%s") + NEWEST_BAN_DATE_SECS=$(date -d "$NEWEST_BAN_DATE_JAIL" +"%s") + + if [ "$OLDEST_BAN_DATE_SECS" -lt "$OLDEST_BAN_DATE" ]; then + OLDEST_BAN_DATE=$OLDEST_BAN_DATE_SECS + fi + if [ "$NEWEST_BAN_DATE_SECS" -gt "$NEWEST_BAN_DATE" ]; then + NEWEST_BAN_DATE=$NEWEST_BAN_DATE_SECS + fi + + BANNED_IPS=$(printf "%s\n%s" "$BANNED_IPS" "$JAILED_IPS") + done + + # Sort banned ips by unban date and time, remove empty lines + BANNED_IPS=$(echo "$BANNED_IPS" | sort -n | grep -v "^$") + + # Format date format for the oldest and newest ban date + OLDEST_BAN_DATE=$(date -d "@$OLDEST_BAN_DATE" +"%Y-%m-%d %H:%M:%S") + NEWEST_BAN_DATE=$(date -d "@$NEWEST_BAN_DATE" +"%Y-%m-%d %H:%M:%S") + + # Calculate the widths + STATS_OLD_W=${#OLDEST_BAN_DATE} + STATS_NEW_W=${#NEWEST_BAN_DATE} + STATS_IP_W=$( + echo "$BANNED_IPS" | + awk -F"|" '{print $3}' | awk '{print length}' | sort -nr | head -n1 + ) + # Calculate the width of the statistics table and add 8 for padding + STATS_W=$((STATS_IP_W + STATS_OLD_W + STATS_NEW_W + 8)) + # Calculate the width of a row in the statistics table and subtract 2 for padding + STATS_R_W=$((STATS_W - 2)) + + # Print the statistics + printf "+-%*s-+\n" $STATS_R_W " " + printf "| %-*s |\n" $STATS_R_W "Statistics" + # Print table separator based on STATS_W + printf "+-%*s-+-%*s-+-%*s-+\n" \ + "$STATS_IP_W" " " \ + "$STATS_OLD_W" " " \ + "$STATS_NEW_W" " " + printf "| %-*s | %-*s | %-*s |\n" \ + "$STATS_IP_W" "Banned IPs" \ + "$STATS_OLD_W" "Oldest ban date" \ + "$STATS_NEW_W" "Newest ban date" + printf "| %-*s | %-*s | %-*s |\n" \ + "$STATS_IP_W" "$UNIQUE_IPS_COUNT" \ + "$STATS_OLD_W" "$OLDEST_BAN_DATE" \ + "$STATS_NEW_W" "$NEWEST_BAN_DATE" + printf "+-%*s-+\n" $STATS_R_W " " + printf "| %-*s |\n" $STATS_R_W "Jails" + printf "| %-*s |\n" $STATS_R_W "$JAILS_TO_LOOP" + printf "+-%*s-+-%*s-+-%*s-+\n" \ + "$STATS_IP_W" " " \ + "$STATS_OLD_W" " " \ + "$STATS_NEW_W" " " + + echo "" + + # Initialize the default guessed widths + local R1W=3 # BAN_NO, start with 3 + local R2W=4 # Jail, sshd might be the most common + local R3W=15 # IP Address, 3+1+3+1+3+1+3=15 (xxx.xxx.xxx) + local R4W=19 # Banned Date, 10+1+8=19 (YYYY-MM-DD HH:MM:SS) + local R5W=8 # Ban Expires, 2+1+2+1+2=8 (HH:MM:SS) + + # Use BANNED_IPS to loop through the banned IPs and get values for the upcoming table + # The table will have the following columns: + # | # | Jail | IP Address | Banned Date | Expires | + # + # Each line of the BANNED_IPS array is in the following format: + # [Unban Date and Time]|[Date and Time Banned]|[IP Address]|[Bantime]|[Jail Name] + + # Init variable and arrays to store the values for the table + local BAN_NO=0 # Incrementing number for each banned IP + local BAN_NO_ARRAY=() # Array to store the incrementing number for each banned IP + local BAN_IP_ARRAY=() # Array to store the IP address of the banned IP + local BAN_BANNED_ARRAY=() # Array to store the date and time the IP was banned + local BAN_REMAINING_ARRAY=() # Array to store the remaining time the IP will be banned + local BAN_JAIL_ARRAY=() # Array to store the jail the IP is banned in + + for ROW in $BANNED_IPS; do + # Increment the BAN_NO + BAN_NO=$((BAN_NO + 1)) + # Get the date and time the IP will be unbanned + local BAN_EXPIRES="" + BAN_EXPIRES=$(echo "$ROW" | awk -F"|" '{print $1}') + # Get the date and time the IP was banned + local BAN_BANNED="" + BAN_BANNED=$(echo "$ROW" | awk -F"|" '{print $2}') + # Get the IP address of the banned IP + local BAN_IP="" + BAN_IP=$(echo "$ROW" | awk -F"|" '{print $3}') + # Get the jails the IP is banned in + local BAN_JAILS="" + BAN_JAILS=$(echo "$ROW" | awk -F"|" '{print $5}') + + # Get the current time in seconds + local CURRENT_TIME="" + CURRENT_TIME=$(date +%s) + # Get the unban time in seconds + local BAN_EXPIRES_SECS="" + BAN_EXPIRES_SECS=$(date -d "$BAN_EXPIRES" +%s) + # Calculate the time remaining until the IP is unbanned + local BAN_REMAINING=$((BAN_EXPIRES_SECS - CURRENT_TIME)) + + # Format the time remaining until the IP is unbanned + local BAN_REMAINING="" + BAN_REMAINING=$(f2b_secs_to_hours_minutes_seconds "$BAN_REMAINING") + + # Get the length of the ban number + local BAN_NO_LENGTH=${#BAN_NO} + # Get the length of the jails + local BAN_JAILS_LENGTH=${#BAN_JAILS} + # Get the length of the IP address + local BAN_IP_LENGTH=${#BAN_IP} + # Get the length of the banned date + local BAN_BANNED_LENGTH=${#BAN_BANNED} + # Get the length of the remaining time + local BAN_REMAINING_LENGTH=${#BAN_REMAINING} + + # Get the length of the longest ban number + if [ "$BAN_NO_LENGTH" -gt "$R1W" ]; then + R1W=$BAN_NO_LENGTH + fi + # Get the length of the longest jails + if [ "$BAN_JAILS_LENGTH" -gt "$R2W" ]; then + R2W=$BAN_JAILS_LENGTH + fi + # Get the length of the longest IP address + if [ "$BAN_IP_LENGTH" -gt "$R3W" ]; then + R3W=$BAN_IP_LENGTH + fi + # Get the length of the longest banned date + if [ "$BAN_BANNED_LENGTH" -gt "$R4W" ]; then + R4W=$BAN_BANNED_LENGTH + fi + # Get the length of the longest remaining time + if [ "$BAN_REMAINING_LENGTH" -gt "$R5W" ]; then + R5W=$BAN_REMAINING_LENGTH + fi + + # Add the values to the arrays for the table + BAN_NO_ARRAY+=("$BAN_NO") + BAN_JAIL_ARRAY+=("$BAN_JAILS") + BAN_IP_ARRAY+=("$BAN_IP") + BAN_BANNED_ARRAY+=("$BAN_BANNED") + BAN_REMAINING_ARRAY+=("$BAN_REMAINING") + done + + # Increase the width of the columns by 2 to allow for padding + H1W=$((R1W + 2)) + H2W=$((R2W + 2)) + H3W=$((R3W + 2)) + H4W=$((R4W + 2)) + H5W=$((R5W + 2)) + + # Print the table + printf " %-${H1W}s %-${H2W}s %-${H3W}s %-${H4W}s %-${H5W}s\n" \ + "#" "Jail" "IP" "Banned" "Expires" + + # Print the table header separator + printf "+-%-${R1W}s-+-%-${R2W}s-+-%-${R3W}s-+-%-${R4W}s-+-%-${R5W}s-+\n" \ + "" "" "" "" "" + + # Loop through the arrays to print the table rows + for ((i = 0; i < ${#BAN_IP_ARRAY[@]}; i++)); do + # Left pad the value of the ban number to the width of the longest ban number + BAN_NO=$(printf "%-${R1W}s" "${BAN_NO_ARRAY[$i]}") + + printf "| %-${R1W}s | %-${R2W}s | %-${R3W}s | %-${R4W}s | %-${R5W}s |\n" \ + "${BAN_NO_ARRAY[$i]}" \ + "${BAN_JAIL_ARRAY[$i]}" \ + "${BAN_IP_ARRAY[$i]}" \ + "${BAN_BANNED_ARRAY[$i]}" \ + "${BAN_REMAINING_ARRAY[$i]}" + done + + # Print the table footer + printf "+-%-${R1W}s-+-%-${R2W}s-+-%-${R3W}s-+-%-${R4W}s-+-%-${R5W}s-+\n" \ + "" "" "" "" "" + echo "" + echo "Expiration time is in days:hours:minutes format." + echo "" +} + +# }}} + +# Check if no arguments are provided or help is requested +if [ $# -eq 0 ]; then + help + exit 0 +fi +case $F2B_ARG1 in +"help") + help + exit 0 + ;; +"version") + echo "$F2B_SCRIPT version $VERSION" + echo "Author: Ismo Vuorinen " + exit 0 + ;; +"list-jails") + echo "$F2B_JAILS" + exit 0 + ;; +esac + +# Use case statement to check for commands: status +if [ "$F2B_ARG1" == "status" ]; then + case $F2B_ARG2 in + "") + echo "Usage: $F2B_SCRIPT status all (to show status of all jails)" + echo " $F2B_SCRIPT status [jail] (to show status of a specific jail)" + echo " Available jails: $F2B_JAILS" + exit 0 + ;; + "all") + $F2B_CLIENT status + exit 0 + ;; + *) + f2b_jail_exists "$F2B_ARG2" + $F2B_CLIENT status "$F2B_ARG2" + exit 0 + ;; + esac +fi + +# Use case statement to check for commands: banned +if [ "$F2B_ARG1" == "banned" ]; then + case $F2B_ARG2 in + "") + echo "Usage: $F2B_SCRIPT banned Show all banned IP addresses with ban time left" + echo " $F2B_SCRIPT banned [jail] Show all banned IP addresses with ban time left in a jail" + echo " Available jails: $F2B_JAILS" + exit 0 + ;; + "all") + f2b_banned_ips all + exit 0 + ;; + *) + # If jail is not in the list, show error + if ! echo "$F2B_JAILS" | grep -q -w "$F2B_ARG2"; then + echo "Error: $F2B_ARG2 not found in: $F2B_JAILS" + exit 1 + fi + f2b_banned_ips "$F2B_ARG2" + exit 0 + ;; + esac +fi + +# Use case statement to check for commands: ban +if [ "$F2B_ARG1" == "ban" ]; then + case $F2B_ARG2 in + "") + echo "Error: Please provide an IP address to ban." + echo "Usage: $F2B_SCRIPT ban [ip] Ban IP address in all jails" + echo " $F2B_SCRIPT ban [ip] Ban IP address in a specific jail" + echo " Available jails: $F2B_JAILS" + exit 1 + ;; + *) + # Ban IP address in all jails + if [ -z "$F2B_ARG3" ]; then + # loop over jails and ban ip in all of them + for JAIL in $F2B_JAILS_ARRAY; do + f2b_ban_ip "$F2B_ARG2" "$JAIL" + done + exit 0 + fi + # Ban IP address in a specific jail + f2b_jail_exists "$F2B_ARG3" + f2b_ban_ip "$F2B_ARG2" "$F2B_ARG3" + exit 0 + ;; + esac +fi + +# Use case statement to check for commands: unban +if [ "$F2B_ARG1" == "unban" ]; then + case $F2B_ARG2 in + "") + echo "Error: Please provide an IP address to unban." + echo "Usage: $F2B_SCRIPT unban [ip] (to unban IP address in all jails)" + echo " $F2B_SCRIPT unban [ip] [jail] (to unban IP address in a specific jail)" + echo " Available jails: $F2B_JAILS" + exit 1 + ;; + *) + # Unban IP address in all jails + if [ -z "$F2B_ARG3" ]; then + # loop over jails and unban ip in all of them + for JAIL in $F2B_JAILS_ARRAY; do + f2b_unban_ip "$F2B_ARG2" "$JAIL" + done + exit 0 + fi + # Unban IP address in a specific jail + f2b_jail_exists "$F2B_ARG3" + f2b_unban_ip "$F2B_ARG2" "$F2B_ARG3" + exit 0 + ;; + esac +fi + +# Use case statement to check for commands: test +if [ "$F2B_ARG1" == "test" ]; then + if [ -z "$F2B_ARG2" ]; then + echo "Error: Please provide an IP address to test." + echo "Usage: $F2B_SCRIPT test [ip] (to test IP address in all jails)" + exit 1 + fi + + # Get list of jails where IP is banned, remove [, ], and quotes + BANNED_IN_JAILS=$($F2B_CLIENT banned "$F2B_ARG2" | sed 's/\[//g; s/\]//g; s/"//g') + echo "IP address $F2B_ARG2 is banned in: $BANNED_IN_JAILS" + exit 0 +fi + +# Use case statement to check for commands: logs +if [ "$F2B_ARG1" == "logs" ]; then + case $F2B_ARG2 in + "") + echo "Usage: $F2B_SCRIPT logs [jail] (to show logs for a specific jail)" + echo " $F2B_SCRIPT logs all (to show logs for all jails)" + echo " $F2B_SCRIPT logs all [ip] (to show logs for a specific IP address in all jails)" + echo " $F2B_SCRIPT logs [jail] [ip] (to show logs for a specific IP address in a specific jail)" + echo " Available jails: $F2B_JAILS" + exit 0 + ;; + "all") + if [ -n "$F2B_ARG3" ]; then + # loop over jails and show logs for all of them + for JAIL in $F2B_JAILS_ARRAY; do + f2b_jail_get_log_entries "$JAIL" "$F2B_ARG3" + done + exit 0 + fi + # loop over jails and show logs for all of them + for JAIL in $F2B_JAILS; do + f2b_jail_get_log_entries "$JAIL" + done + exit 0 + ;; + *) + # Show logs for a specific jail + f2b_jail_exists "$F2B_ARG3" + f2b_jail_get_log_entries "$F2B_ARG2" + exit 0 + ;; + esac +fi + +# Use case statement to check for commands: logs-watch +if [ "$F2B_ARG1" == "logs-watch" ]; then + case $F2B_ARG2 in + "") + echo "Usage: $F2B_SCRIPT logs-watch [jail] (to watch logs for a specific jail)" + echo " $F2B_SCRIPT logs-watch all (to watch logs for all jails)" + echo " $F2B_SCRIPT logs-watch all [ip] (to watch logs for a specific IP address in all jails)" + echo " $F2B_SCRIPT logs-watch [jail] [ip] (to watch logs for a specific IP address in a specific jail)" + echo " Available jails: $F2B_JAILS" + exit 0 + ;; + "all") + if [ -n "$F2B_ARG3" ]; then + # loop over jails and watch logs for all of them + for JAIL in $F2B_JAILS_ARRAY; do + f2b_poll_jail_log_entries "$JAIL" "$F2B_ARG3" + done + exit 0 + fi + # loop over jails and watch logs for all of them + for JAIL in $F2B_JAILS; do + f2b_poll_jail_log_entries "$JAIL" + done + exit 0 + ;; + *) + # Watch logs for a specific jail + f2b_jail_exists "$F2B_ARG3" + f2b_poll_jail_log_entries "$F2B_ARG2" + exit 0 + ;; + esac +fi + +# Use case statement to check for commands: service +if [ "$F2B_ARG1" == "service" ]; then + case $F2B_ARG2 in + "start") + echo "Starting fail2ban service..." + sudo service fail2ban start + exit 0 + ;; + "stop") + echo "Stopping fail2ban service..." + sudo service fail2ban stop + exit 0 + ;; + "restart") + echo "Restarting fail2ban service..." + sudo service fail2ban stop + sudo service fail2ban start + exit 0 + ;; + "status") + echo "Checking fail2ban service status..." + sudo service fail2ban status + exit 0 + ;; + *) + echo "Usage: $F2B_SCRIPT service [start|stop|restart|status]" + exit 1 + ;; + esac +fi + +# If first argument is test-filter, run test-filter command +if [ "$F2B_ARG1" == "test-filter" ]; then + F2B_REGEX_COMMAND="command -v fail2ban-regex" + F2B_REGEX_SUDOED="sudo $F2B_REGEX_COMMAND" + if [ -z "$F2B_REGEX_COMMAND" ] || [ ! -x "$F2B_REGEX_COMMAND" ]; then + echo "Error: fail2ban-regex command not found." + exit 1 + fi + if [ -z "$F2B_ARG2" ]; then + F2B_FILTERS=$(sudo ls /etc/fail2ban/filter.d/ | sed 's/\.conf//g' | tr '\n' ' ') + echo "Error: Please provide a filter to test." + echo "Usage: $F2B_SCRIPT test-filter [filter]" + echo " Available filters: $F2B_FILTERS" + exit 1 + fi + F2B_FILTER_FILE="/etc/fail2ban/filter.d/$F2B_ARG2.conf" + if [ ! -f "$F2B_FILTER_FILE" ]; then + echo "Error: $F2B_ARG2 filter not found." + exit 1 + fi + # Get log path from filter file + F2B_LOG_PATH=$(grep -i "logpath" "$F2B_FILTER_FILE" | awk '{print $3}') + if [ -z "$F2B_LOG_PATH" ]; then + echo "Error: logpath not found in: $F2B_FILTER_FILE" + exit 1 + fi + # Get regex from filter file + F2B_REGEX=$(sudo grep -i "failregex" "$F2B_FILTER_FILE" | + awk '{for(i=2;i<=NF;++i) printf "%s ", $i}') + if [ -z "$F2B_REGEX" ]; then + echo "Error: failregex not found in: $F2B_FILTER_FILE" + exit 1 + fi + # Test filter + echo "Testing filter: $F2B_ARG2" + echo "- Filter file: $F2B_FILTER_FILE" + echo "- Log path: $F2B_LOG_PATH" + echo "- Regex: $F2B_REGEX" + $F2B_REGEX_SUDOED "$F2B_LOG_PATH" "$F2B_REGEX" + + unset F2B_REGEX_COMMAND F2B_REGEX_SUDOED F2B_FILTERS F2B_FILTER_FILE F2B_LOG_PATH F2B_REGEX +fi + +# Show help if no valid command is provided +help +exit 0