#!/bin/bash # tail2notify v0.3.4 last mod 2011/12/27 # Latest version at # Copyright 2011 Ryan Sawhill # # 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 # 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. #------------------------------------------------------------------------------ # The simple purpose here is to pipe tail -F to notify-send in order to get # desktop notifications when certain things happen in a designated log file. # Both the log file and search string are configurable from the command-line. # # The general idea would be that you run this from your desktop environment # (via a shortcut or your file manager or a terminal). If you don't have rights # to read the log file you specify, the script will use a gui sudo program if # you have one or otherwise try sudo, if possile. If that doesn't work, it will # print out a message telling you how to solve the problem. My favorite way: # setfacl -m u:$USER:r LOGFILE # That way you don't have to configure sudo or type a password each time. # # This project found its genesis as a result of a forum question asking for a # way to get a desktop notification on every ssh login, so... as the help page # will tell you, if you run the script with no arguments, it defaults to # watching /var/log/secure (or /var/log/auth.log) for any successful or failed # sshd logins and displays them nicely with notify-send. # # Feedback very much appreciated! Contact info above. #------------------------------------------------------------------------------ # A CONVENIENCE VARIABLE (same as running `basename $0`) zero=${0##*/} # ERROR CODES e_MISSING_notify_send=113 e_NO_DISPLAY=106 e_BAD_LOGNAME=79 e_BAD_ARGV=80 e_AUTH_FAILED=101 e_FAILED_TO_START_TAIL=99 # Note: I use urgency of critical because notifications disappear too quickly # on my Fedora desktop otherwise -- other choices are low & normal notifysend_urgency='critical' #============================================================================== #------------------------------------------ CHECK PRE-REQS, CONFIGURE ENV ----> # BAIL IF DON'T HAVE notify_send OR IF $DISPLAY NOT SET OR IF RUNNING w/ SU if ! command -v notify-send >/dev/null; then printf "ERROR\nnotify-send does not appear to be installed on your system.\nThe purpose of this script is to use the 'notify-send' utility to display\ndesktop notifications for log-file activity.\n" >&2 exit $e_MISSING_notify_send elif [[ -z $DISPLAY ]]; then printf "ERROR\n\$DISPLAY not set. Simply put, you must run this from a terminal or shortcut in\nyour desktop environment.\n" >&2 exit $e_NO_DISPLAY elif [[ $(logname) != $USER ]]; then printf "ERROR\nYou must run this script as the user logged in to your desktop. You can't run\nit as root unless root is logged into the desktop environment.\n" >&2 exit $e_BAD_LOGNAME fi # PID FILE FOR ALL tail2notify INSTANCES # Here we create and manage a file containing PIDs of all our running instances # of the tail2notify script global_pidlist_file=$HOME/.$zero.pids # Garbage collection [[ -f $global_pidlist_file ]] && for pid in $(<$global_pidlist_file); do [[ -e /proc/$pid ]] && break sed -i /$pid/d $global_pidlist_file done printf "$$\n" >>$global_pidlist_file trap "sed -i /$$/d $global_pidlist_file" EXIT # VARIABLES -- don't actually need to unset most of them, but it's a good habit # to get into & it makes it easy for you see what variables we use unset simple mode search_str logfile pwdreq tail_log Gsudo tstamp prev_tstamp ip prev_ip msg prev_msg i success title # CHECK FOR HELP QUERY usage="Usage: $zero [--ssh|--vpn]\n or: $zero [--simple|--simplest] 'GREP_SEARCHSTRING' [LOGFILE]\n or: $zero -a LOGFILE\n" case ${1##*-} in \?) printf "$usage"; exit ;; help|h) printf "$usage Use tail -F and grep to watch for logfile events and send notifications to the desktop via the notify-send utility. With no arguments, $zero defaults to watching for sshd activity. If using a custom search string and no logfile is specified, LOGFILE defaults to /var/log/messages. Options:\n" printf " --ssh@watches /var/log/secure or /var/log/auth.log for ssh login attemps --vpn@watches /var/log/messages for a crumbling openvpn connection --simple@prevents any attempt at specially formatting input --simplest@like --simple, but also shows duplicate messages -a@used alone, this option is an alias for --simplest '.*'" | column -s@ -t printf " If you don't use one of --ssh/--vpn/-a, you must specify a search string, which is passed to grep, so simple regex is possible. As an EXAMPLE, you could try: $zero 'parrot\|lumberjack' Then run 'logger dead parrot' or 'logger hes a lumberjack' to see it in action. Next try the same with --simple and --simplest to see how that changes output. These next two EXAMPLES are identical (the -a option is an alias for '--simplest .*') and will generate notifications for every new line added: $zero --simplest '.*' FILE $zero -a FILE Error codes: $(grep '^e_.*=' $0 | sed 's/^/ /') Version info: $(head -n2 $0 | grep -o 'tail2notify v.*') Report bugs or suggestions to Or see for bug tracker & latest version\n" exit ;; esac # CHECK FOR ONE OF **SIMPLE** OPTIONS AS FIRST PARAMETER if [[ $1 == --simple || $1 == --simplest ]]; then simple=${1##--} shift [[ $# -eq 1 || $# -eq 2 ]] || { printf "Error: need more arguments\n$usage\nRun with --help\n" >&2 exit $e_BAD_ARGV; } fi # TIME TO PARSE THROUGH ARGUMENTS # First, if NO args or if run with --ssh, set our mode, searchstr, & logfile if [[ $# -eq 0 || $1 == --ssh ]]; then # Watches for any successful or failed ssh logins search_str='sshd.*Accepted\|sshd.*Failed' # File different on different distros -- contact me to add more plz! for logfile in /var/log/secure /var/log/auth.log; do [[ -f $logfile ]] && break done mode=ssh [[ $# > 1 ]] && printf "Extra argument(s) ignored\n" >&2 # If first argument is --vpn, set our mode, searchstr, & logfile accordingly elif [[ $1 == --vpn ]]; then # Watches for early signs of the demise of a vpn connection search_str='openvpn.*ECONNREFUSED\|openvpn.*Inactivity.timeout\|openvpn.*HOST_NOT_FOUND' logfile=/var/log/messages mode=messages [[ $# > 1 ]] && printf "Extra argument(s) ignored\n" >&2 # Here you can easily customize the script yourself, adding a custom preset #elif [[ $1 == --YourCustomOptionHere ]]; then #search_str=SetACustomSearchString #logfile=SetAFile # Choose a mode for your preset: #mode=messages #mode=simple #mode=simplest #[ $# > 1 ]] && printf "Extra argument(s) ignored\n" >&2 elif [[ $1 == -a ]]; then search_str='.*' logfile=$2 [[ -f $logfile ]] || { printf "Error: logfile '$2' does not exist\n$usage\nRun with --help\n" >&2 exit $e_BAD_ARGV; } mode=simplest [[ $# > 2 ]] && printf "Extra argument(s) ignored\n" >&2 # If only 1 argument (and it wasn't one of the above), then assume it's a # search string and that user wants to follow /var/log/messages # Also, check to see if user asked for one of the simple modes elif [[ $# -eq 1 ]]; then search_str=$1 logfile=/var/log/messages [[ -n $simple ]] && mode=$simple || mode=messages # If user provided 2 commandline args, check to make sure the second is a file # and assume the first is the search string elif [[ $# -eq 2 ]]; then search_str=$1 logfile=$2 [[ -f $logfile ]] || { printf "Error: logfile '$2' does not exist\n$usage\nRun with --help\n" >&2 exit $e_BAD_ARGV; } [[ -n $simple ]] && mode=$simple || mode=messages else printf "Error: improper argument(s)\n$usage\nRun with --help\n" >&2 exit $e_BAD_ARGV fi # TAIL COMMAND # We're about to figure out our tail command, but let's break it down first: # tail -Fn0 --pid=$$ $logfile # -F is the same as (--folow=name --retry) which means we follow the file NAME # --pid=$$ is important: It ensures that the tail and the rest of its chain stop # if our script exits # SO, if we can read our logfile, then our tail command is simple if [[ -r $logfile ]]; then tail_log="tail -Fn0 --pid=$$ $logfile" # Otherwise, we need to figure out how we're going to get privileges else # If we're on a tty and sudo tail can be run with those opts, use that if sudo -l tail -Fn0 --pid=$$ $logfile >/dev/null; then tail_log="sudo tail -Fn0 --pid=$$ $logfile" # If we have to run sudo tail with a password, set a variable for later sudo -l | grep -q '(root) NOPASSWD: .*tail\|(root) NOPASSWD: ALL' || pwdreq=yes # Else, try to look for a gui sudo program # Please contact me if this part isn't working for you so I can fix it! else for Gsudo in beesu kdesu gksu ktsuss NO_Gsudo; do command -v $Gsudo >/dev/null && break ; done [[ $Gsudo != NO_Gsudo ]] && tail_log="$Gsudo tail -Fn0 --pid=$$ $logfile" pwdreq=yes fi fi # If none of that worked, time to throw an error [[ -z $tail_log ]] && { notify-send -u $notifysend_urgency "$zero: Error - Couldn't open $logfile" "OPTIONS:\n(1) Run 'setfacl -m u:$USER:r $logfile' as root, giving $USER the permanent ability to read it\n(2) Configure sudo so $USER can run, at the least, '/usr/bin/tail -Fn0 *', preferably with NOPASSWD\n(3) Install beesu, kdesu, or gksu so $zero can authenticate you as root from a desktop session" exit $e_AUTH_FAILED ;} #============================================================================== #------------------------------------------------------ KICK OFF THE TAIL ----> # IF YOU'RE JUST READING TO GET A QUICK FEEL FOR WHAT THE SCRIPT IS DOING: # skip down to the bottom of this section and check out Watch_simplest() # After the first few versions of this script, I made the following a function # in order to make it possible to easily watch other logfiles Watch_sshd(){ # Run our tail command, piped to... $tail_log | # grep -- for sshd lines that say Accepted or Failed, then pipe to... grep --line-buffered "$search_str" | # while/read -- to loop through data one line at a time while read line; do # Here's an example line that our tail | grep could return: # Dec 18 06:39:47 apriori sshd[30809]: Accepted publickey for ryran from 127.0.0.1 port 35995 ssh2 # So, we'll use bash variable substring magic to pull out pieces of it # 1st, the timestamp: simply delete " HOSTNAME" and everything after it prev_tstamp=$tstamp; tstamp=${line/ $(hostname)*/} # The ipaddr is more annoying & less rock-solid, but I'm OK with that # We delete everything on the left of the ipaddr (up to and including # the "from ") and then from the right to left (up to and including " port") # We could of course use something heavier like grep to more perfectly # pull out the the ip, but I'm happy with this lightweight solution. prev_ip=$ip; ip=${line#*from }; ip=${ip% port*} # The message is everything after the first occurence of "]: " # (as in, HOSTNAME sshd[30809]: MESSAGE) prev_msg=$msg; msg=${line#*]: } # Now we've got our variables, so... First we test to see if we're # dealing with a repeat failed login attempt message # If the first word of our message is Failed && # the previous & current message (minus port info) are the same if [[ ${msg%% *} == Failed && ${prev_msg% port*} == ${msg% port*} ]]; then i=$(($i+1)) # Increment identical_message counter by 1 # If counter has reached 3, time to print a message, so basically # this all boils down to: # * The 1st time someone tries to login and fails, you get notified # * The 2nd & 3rd times you don't get notified # * If there's a 4th time, you get warned # * If there's a 5th time, it's as if we're starting over at step 1 [[ $i -eq 3 ]] && { i=0 notify-send -u $notifysend_urgency "sshd: 3 more failed login attempts by $ip" "Data for last occurence:\n[$tstamp] $msg" ;} # If we're not dealing with repeating failed login attempts... else i=0 # Set counter to 0 # Run different commands based on the first word of our message: case ${msg%% *} in Accepted) notify-send -u $notifysend_urgency "sshd: New session initiated by $ip" "[$tstamp] $msg" ;; Failed) notify-send -u $notifysend_urgency "sshd: Failed login attempt by $ip" "[$tstamp] $msg" ;; esac fi done & # Send that whole chain of commands into the background so we can continue } # Here we have flexible watching function, which is tailored to take advantage # of the kind of lines you usually see in e.g. /var/log/messages # The parts that are different from above are commented Watch_messages(){ $tail_log | grep --line-buffered "$search_str" | while read line; do prev_tstamp=$tstamp; tstamp=${line/ $(hostname)*/} prev_msg=$msg; msg=${line/*$(hostname) /} # Try to auto-detect the program that generated the logentry title=${line%%[*} # If that's not working, set title to the name of our logfile [[ $title == $line ]] && title=$logfile || title=${title/*$(hostname) /}; # If we've got a duplicate message, let's ONLY increment our counter if [[ $prev_msg == $msg ]]; then i=$(($i+1)) # If message is not a duplicate, but our counter is greater than zero, # we got our last duplicate (and obviously a new message) elif [[ $i > 0 ]]; then # Print repeated message notify-send -u $notifysend_urgency "$title repeated msg $i times" "Last occurence @ $prev_tstamp:\n$prev_msg" i=0 # Print the most recent (non-repeated) message notify-send -u $notifysend_urgency "$title @ $tstamp" "$msg" # Else, we're not dealing with repeating messages at all else notify-send -u $notifysend_urgency "$title @ $tstamp" "$msg" i=0 fi done & } # And here's a simple version that doesn't do any processing of the strings, # but does stack duplicate messages # No comments.. nothing new compared to above Watch_simple(){ $tail_log | grep --line-buffered "$search_str" | while read line; do prev_msg=$msg; msg=$line if [[ $prev_msg == $msg ]]; then i=$(($i+1)) elif [[ $i > 0 ]]; then notify-send -u $notifysend_urgency "$logfile repeated msg $i times" "$prev_msg" i=0 notify-send -u $notifysend_urgency "$logfile" "$msg" else notify-send -u $notifysend_urgency "$logfile" "$msg" i=0 fi done & } # Finally: the simplest version of all, which passes every line to notify-send Watch_simplest(){ $tail_log | grep --line-buffered "$search_str" | while read line; do notify-send -u $notifysend_urgency "$logfile" "$line" done & } # Now that figured out what the user wants and configured all our variables and # defined all our functions, time to run the right thing (based on $mode which # was set at the beginning by our command-line args [or lack thereof]) case $mode in ssh) Watch_sshd ;; messages) Watch_messages ;; simple) Watch_simple ;; simplest) Watch_simplest ;; esac #============================================================================== #--------------------------------------------------------------- REACTING ----> # This section checks to make sure we successfully started a tail on $logfile # If we're using sudo w/a password, or using one of the gui sudo apps, we need # to wait for the root-user tail process to start (i.e., for user to enter pwd) if [[ $pwdreq == yes ]]; then # Keep this loop going as long as our "sudo tail" process is still running while ps --ppid $$ --no-header -o args | head -n1 | grep -q -- "tail.*pid=$$"; do if ps -C tail --no-header -o args | grep -q -- "^tail -Fn0 --pid=$$"; then # If our sudo-spawned tail process is running, we're finished, so break! break else # Otherwise, wait sleep 1 fi done else # If no password is required, wait 1s to ensure tail has time to start sleep 1 fi # Show all children { ps --ppid $$ -o ppid,pid,args if [[ $pwdreq == yes ]]; then echo ps --no-header -C tail -o ppid,pid,args | grep "pid=$$" fi } >&2 # Depending on whether tail has been started, print success or fail if ps --no-header -o args -C tail | grep -q -- "^tail -Fn0 --pid=$$"; then if tty --quiet; then # Messages to print if run from an interactive terminal notify-send "$zero monitoring $logfile" "Successfully initialized monitoring process. You will find a list of PIDs for $zero processes in ~/${global_pidlist_file##*/}." printf "\nBegan monitoring file '$logfile' for search string '$search_str' ...\n" >&2 else notify-send "$zero monitoring $logfile" "Successfully initialized monitoring process. A simple 'killall $zero' will shut it down. You will also find a list of PIDs for $zero processes in ~/${global_pidlist_file##*/}." fi else if tty --quiet; then # Messages to print if run from an interactive terminal notify-send -u $notifysend_urgency "$zero ERROR" "There was an error initializing $logfile monitoring process and $zero shut down." printf "\nError initializing monitoring of file '$logfile' for search string '$search_str'\n" >&2 else notify-send -u $notifysend_urgency "$zero ERROR" "There was an error initializing $logfile monitoring process and $zero shut down. Running $zero from a terminal might help you diagnose the problem." fi exit $e_FAILED_TO_START_TAIL fi # Wait (indefinitely) for our background tail process & friends to finish wait