feat: first version of the script

This commit is contained in:
2024-08-27 22:06:33 +03:00
parent 65fc1cb2bb
commit 68e416c547
7 changed files with 1082 additions and 4 deletions

66
.github/README.md vendored Normal file
View File

@@ -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/)

164
.github/labels.yml vendored Normal file
View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -15,4 +15,4 @@ permissions:
jobs:
stale:
uses: ivuorinen/.github/.github/workflows/stale.yml@main
uses: ivuorinen/ivuorinen/.github/workflows/stale.yml@main

View File

@@ -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

848
f2b Executable file
View File

@@ -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 <https://github.com/ivuorinen> (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 <jail> <ip>
# 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 <jail>
# 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 <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] <jail>
# 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 <jail> 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 <jail> 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 <https://github.com/ivuorinen>"
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] <jail> 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