#!/bin/bash
# $File: modseclogs $
# $Author: Marko Schulz <info@tuxnet24.de> $
# $Date: 2023-09-30 21:55:14 +0200 (So, 30 Sep 2023) $
# $Description: Script to pretty display mod_security logfiles. $
# *******************************************************

# [GLOBAL CONFIGURATION]
#
# Configuration variables as cfg assoc array
declare -A cfg

# Path to the logfile directory
cfg[logdir]="/var/log/apache2"

# Display the last cfg[num] lines of the logfile
cfg[num]=3

# Output template for jq command for the normal view
cfg[output,normal]=$(cat <<TXT1
"Timestamp: " +.transaction.time,
"Host: "+.request.headers.Host,
"RemoteAddress: "+.transaction.remote_address,
"Request: "+.request.request_line,""+"ErrorMessage:",
.audit_data.messages,""
TXT1
)

# Output template for jq command for the verbose view
cfg[output,verbose]=$(cat <<TXT2
"Timestamp: " +.transaction.time,
"Host: "+.request.headers.Host,
"RemoteAddress: "+.transaction.remote_address,
"Request: "+.request.request_line,
"Referer: "+.request.Referer,
"User-agent: "+.request."User-agent",
"Accept-Encoding: "+.request."Accept-Encoding",
"Content-Type: "+.request."Content-Type",
"Body: ",.request.body,"ErrorMessage:",
.audit_data.messages,""
TXT2
)

# *******************************************************
# This function check if the item is an integer.
#
# @param $1 - The item that will be checked
# @return bool
#
function is_integer () {

if ! [[ "$1" =~ ^[0-9]+$ ]]; then
    return 1
else
    return 0
fi

}

# *******************************************************
# This function display help/usage and exit the program.
# If an argument exists (error message) the program will
# by exit with exit code 1.
#
# @param $1 - error message (optional)
# @param $2 - exit code (optional, default is 0)
# @return void
#
function usage () {

local stderr=$1
local ecode=${2:-0}

# Print out error message, if defined
if [ -n "${stderr}" ]; then
  echo -e "ERROR: ${stderr}\n"
  # If no exit code defined, we set them to 1, on error
  [ $ecode -eq 0 ] && ecode=1
fi

echo -e "\aUsage: $( basename $0 ) [ -n <num> -v ] <vhostname>\n"
echo -e "\t-n display the last number of lines (default: ${cfg[num]})"
echo -e "\t-v display more request header informations"
echo -e "\t-h show this Help\n\a"
exit ${ecode}

}

# *******************************************************
# This function get the json data from STDIN and make
# the output mutch pretty e.g. colorize the id.
#
# @return string
#
function showlogs () {

cat - | perl -pne '
BEGIN {
  sub colorize {
    my ($tx, $color) = @_;
    my @c = ("32", "1;33", "1;34");
    chomp $tx;
    sprintf ("\e[$c[$color]m%s\e[0m", $tx);
  }
}

s/^([\w-]+)\:(.+)?$/@{[colorize($1,2)]}\:$2/g;
s/^(\s+)(.+\s+\[id\s+\\")(\d+)(\\"\].+)$/\1@{[colorize($2,0)]}@{[colorize($3,1)]}@{[colorize($4,0)]}/g;

'

}

# *******************************************************
# [MAIN]

# Default values
ARG_NUMBER=${cfg[num]}
ARG_VERBOSE="normal"

while getopts n:hv Optionen 2>/dev/null; do
    case $Optionen in
        n) ARG_NUMBER=$OPTARG    ;;
        v) ARG_VERBOSE="verbose" ;;
        h) usage                 ;;
    esac
done

# Get additional arguments, ARG1 is the Servername
ARG1=${@:$OPTIND:1}

# Error, if no Servername (ARG1) defined
if [ ! -f "${cfg[logdir]}/${ARG1}_modsec.log" ]; then
    usage "No servername was defined, no logfile found!" 1
fi

# Error, if no number (ARG_NUMBER) is not a integer
if ! is_integer $ARG_NUMBER ; then
    usage "You have to insert a valid digit!" 1
else
    num=$ARG_NUMBER
fi

tail -n $num ${cfg[logdir]}/${ARG1}_modsec.log | \
    jq -r "${cfg[output,$ARG_VERBOSE]}" | \
    showlogs

# vim: syntax=bash ts=2 sw=2 sts=2 sr noet
