#!/usr/bin/env bash # Nagios plugin for auditd # Copyright © 2021 henrik lindgren # # 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. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Check for anomaly's, failed logins, systemcalls and more return the data suitable for pnp4nagios # # tested on Centos7,8, Fedora33, Fedora39 xmltable(){ local iter=1 local table='' while read -r key value warn critical min max; do [[ $iter -eq 1 ]] && table="${table}" [[ $iter -gt 1 ]] && table="${table}" ((iter+=1)) done echo "$table
$key$value$warn$critical$min$max
$key$value${warn}$critical$min$max
" } usage () { local SPATH SPATH="$( realpath "$0" 2>/dev/null )" echo " Disclamer: Beware this plugin allows injection of shell code by design! Please make sure you have a throughout understanding of auditd and its implementation before using this plugin. Usage: $1 [OPTION] -a,--auargs Extra arguments passed on to aureport -A,--ausargs Extra arguments passed on to ausearch -F,--checkpoint File used to store audit checkpoint, defaults to /tmp/.checkpoint -C,--nocheckpoint [] Do not create checkpoint file, instead use , useful for debugging, see 'man ausearch -ts' for time format, defaults to 'recent' -x,--maxage Max age in seconds after wich checkpoint file is invalidated in seconds. this value should be somewhat larger than nagios check_interval, defaults to 800 seconds -w,--warn Global fall-back value to use if [warn] is not defined -c,--critical Global fall-back value to use if [crit] is not defined -s,--ignore comma-separated list of items or bash-regex to ignore -n,--nometrics disable metrics in output, useful if not using pnp4nagios -v,--verbose Show metrics more verbosely in a list --[a-zA-Z]= Options containing a key from the output of '$1 -v' preceded by comma separated list [warn],[crit],[min],[max] --rules Number of audit rules as shown by 'auditctl -l | wc -l' --lost Number of lost audit events see 'auditctl -s' --backlog Number of audit events in backlog -X Output xmltable, if using '-v'. This can be enabled in nagios with -h,--help Show this message Examples: Logins and failed logins $1 --failedlogins=2,1,0,10 --logins=200,300,0,1000 -v To only return failed events $1 -a '--failed' --failedlogins=2,1,0,10 --failedauthentications=10,14,0 -v --ignore faileddogs,failedhounds Show passwords for users that attempted to login with their username set to their password (for older auditd) and usernames of failed login attempts $1 -a '--auth --failed' -v -n Return failed commands, this might break rrdtool database use '-n'!! $1 -a '--comm --failed' -A '-x ' -v -n Return failed systemcalls $1 -a '-s --failed' -v -n Setup: add following to /etc/sudoers or /etc/sudoers.d/nagios nagios ALL=(root:ALL) NOPASSWD:$SPATH Nagios service if using check_by_ssh: define service { use local-service service_description auditd hostgroup_name linux-servers# aureport has a feature that requires it to be started as a coproc over ssh check_command check_by_ssh!/usr/bin/sudo \$USER1\$/check_auditd -v -a '--failed' &! check_interval 10 register 1 } " } # declare hash to keep commandline values in declare -A opthash=(); # ensure that we dont loop infinitly guard=30 # remove equal signs from $@ #@="${@//=/ }" while [[ $guard -gt 0 ]] do ((guard-=1)) case "${1}" in -h|--help) usage "$0" ; exit 3 ;; -v|--verbose) VERBOSE=1 ; shift ; continue;; -n|--nometrics) NOMETRICS=1 ; shift ; continue;; -a|--auargs) AUARGS="${2}" ; shift 2 ; continue;; -A|--ausearchargs) AUSEARCHARGS="${2}" ; shift 2 ; continue;; -s|--ignore) ignore="${2//,/ }" ; shift 2 ; continue;; -w|--warning) fallback_warning="${2}" ; shift 2 ; continue;; -c|--critical) fallback_critical="${2}" ; shift 2 ; continue;; -m|--min) min="${2}" ; shift 2 ; continue;; -M|--max) max="${2}" ; shift 2 ; continue;; -F|--checkpointfile) CHECKPOINTFILE="${2:-/tmp/.checkpoint}" ; shift 2 ; continue;; -x|--maxage) MAXCHECKPOINTAGE="${2}" ; shift 2 ; continue;; -X|--xmltable) xmltable="1" ; shift 1 ; continue;; -C|--nocheckpoint) NOCHECKPOINT="${2}" ; shift 2 ; continue;; --[a-zA-Z][a-zA-Z=]*) # grab long-options saving opt as hash key and arg as its value opt="${1#*--}" if [[ ! "$2" =~ ^- ]] ; then # match --key 1,2,3,4 opthash[$opt]="${2//[^0-9csBuTMKGm%.]/,}" shift 2; continue elif [[ "$2" =~ ^- ]] ; then # this should match --key=1,2,3,4 value="${opt#*=}" opthash[${opt%%=*}]="${value//[^0-9csBuTMKGm%.]/,}" shift 1; continue fi ;; *) # everything else, end of input reading shift; break ;; esac done [[ -n $VERBOSE ]] && VERBOSE='\nkey\tvalue\twarn\tcritical\tmin\tmax\n' OK='OK - ' CRITICAL='CRITICAL - ' WARN='WARNING - ' UNKNOWN='UNKNOWN - ' STATUS="$OK" # Plugin return code CODE=0 AUBIN='/usr/sbin/aureport' CHECKPOINTFILE=${CHECKPOINTFILE:-/tmp/.checkpoint} MAXCHECKPOINTAGE=${MAXCHECKPOINTAGE:-760} CHECKPOINTAGE=0 if [[ -z $NOCHECKPOINT && -f $CHECKPOINTFILE && -w $CHECKPOINTFILE ]] ; then # if checkpointfile is writable and should be updated CHECKPOINTAGE=$(( $(date +%s) - $(awk -F'[=:. ]' '/^output/{print $3}' "${CHECKPOINTFILE}" 2>/dev/null) )) if [[ ${CHECKPOINTAGE} -lt ${MAXCHECKPOINTAGE} ]]; then AUSEARCH="/usr/sbin/ausearch --raw ${AUSEARCHARGS} -ts checkpoint --checkpoint ${CHECKPOINTFILE}" else # delete checkpointfile to avoid error rm -f "$CHECKPOINTFILE" &> /dev/null recent=$(( $(date +%s) - MAXCHECKPOINTAGE )) # TODO: This might need fixing to support different locales recent=$( date +'%x %H:%M:%S' -d @$recent 2>/dev/null) AUSEARCH="/usr/sbin/ausearch --raw ${AUSEARCHARGS} -ts ${recent:-recent} --checkpoint ${CHECKPOINTFILE}" fi elif [[ ! -f $CHECKPOINTFILE && -z $NOCHECKPOINT ]]; then # if checkpointfile not exitst and should be created # timestamp - maxcheckpointage in seconds recent=$(( $(date +%s) - MAXCHECKPOINTAGE )) # TODO: This might need fixing to support different locales recent=$( date +'%x %H:%M:%S' -d @${recent} 2>/dev/null) AUSEARCH="/usr/sbin/ausearch --raw ${AUSEARCHARGS} -ts ${recent:-recent} --checkpoint ${CHECKPOINTFILE}" elif [[ -z $NOCHECKPOINT && -f $CHECKPOINTFILE && ! -w ${CHECKPOINTFILE} ]] ; then echo "${UNKNOWN}user: $USER can not write to $CHECKPOINTFILE, fix permissions or run $0 as different user" exit 3 else # if no checkpoint file should be created AUSEARCH="/usr/sbin/ausearch --raw ${AUSEARCHARGS} -ts ${NOCHECKPOINT:-recent}" fi set -o pipefail # save output of aureport as newline delimited 'key value' pairs to $AUREPORT # shellcheck disable=2086 # use wordsplitting as a feature AUREPORT="$( ${AUSEARCH} | $AUBIN --summary -i ${AUARGS} | awk -v'FS=: ' ' /^[0-9]/ && /[0-9a-zA-Z]$/ {split($0,values,/\s+/); print values[2],values[1] } /^Number of/ { nr=$2; FS=": " gsub(/(Number of )|\W|[0-9]/," ",$0); gsub(/\s+/,"",$0); print $1,nr} END {print "dummy",0}' 2>/dev/null )" austatus=$? if [[ $austatus -gt 0 ]]; then AUSEARCH="$AUSEARCH --raw ${AUSEARCHARGS} \| aureport --summary -i ${AUARGS} \| awk ..." echo "$UNKNOWN '$AUSEARCH' returned status $austatus, check stderr, permissions and arguments passed to $0" exit 3 fi # check nr of running audit rules if [[ $EUID -eq 0 ]]; then rules=$( /usr/sbin/auditctl -l 2>/dev/null | grep -c -v '^No rules' 2>/dev/null ) if [[ ${PIPESTATUS[0]} -eq 0 ]]; then AUREPORT="${AUREPORT} rules ${rules:-null}" fi # check audit daemon status, metrics while read -r key value; do AUREPORT="${AUREPORT} $key $value" done <<< "$(/usr/sbin/auditctl -s 2>/dev/null | awk '/^(pid|lost|backlog)\s[0-9]+$/ {print $1,$2}' 2>/dev/null)" else # fallback check if audit daemon is running if ! pgrep -x -u 0 'auditd' &> /dev/null ; then echo "$CRITICAL auditdaemon not running, $0 invoked as user: $USER" exit 1 fi fi # set checkpoint age AUREPORT="$AUREPORT checkpointfileage ${CHECKPOINTAGE:-0}" [[ -z ${opthash['checkpointfileage']} ]] && opthash['checkpointfileage']=",,0,$MAXCHECKPOINTAGE" # dummy value to guard against empty result sets ignore="$ignore dummy" # read key value pairs separated by space one line at a time while read -r key value; do sign='' IFS=',' read -r warn critical min max <<< "${opthash[$key]}" # shellcheck disable=2199 disable=2076 [[ $key == 'dummy' || " ${ignore[*]} " =~ " $key " ]] && continue # avoid fail if input is empty or in ignore list # TODO: move this out of the loop if [[ $key == 'rules' && $value -le 0 ]]; then STATUS="$WARNING no configured audit rules " CODE=1 sign='!' elif [[ $key == 'pid' && $value -le 0 ]]; then STATUS="$CRITICAL audit daemon not running " CODE=2 sign='!!' fi [[ $key != 'checkpointfileage' ]] && if [[ -n $critical && $value -ge ${critical:-$fallback_critical} ]] ; then STATUS="$CRITICAL" CODE=2 sign='!!' # prepend critical metrics with !! elif [[ -n $warn && $CODE -ne 2 && $value -ge ${warn:-$fallback_warning} ]] ; then STATUS="$WARN" CODE=1 sign='!' # prepend warning metrics with ! elif [[ -z $value || $value -lt 0 ]] ; then STATUS="$UNKNOWN metric out of bounds: " value='null' CODE=3 sign='?' # prepend to not sane metrics fi if [[ $key == 'events' ]] ; then # prepend 'event' metrics="$key=$value;$warn;$critical;$min;$max ${metrics}" # use pnp4nagios UoM else #[[ $key =~ (lost)|(backlog) ]] && value="${value}c" # TODO metrics="${metrics} $key=$value;$warn;$critical;$min;$max" # use pnp4nagios UoM fi if [[ $sign =~ '!' && $key != 'checkpointfileage' || $key == 'events' ]] ; then # show critical and warnings first shortmetrics="$key=${value}${sign} ${shortmetrics}" # show after status OK - shortmetrics... else [[ $value -gt 0 && $key != 'checkpointfileage' ]] && shortmetrics="${shortmetrics} $key=${value}" # show after status OK - shortmetrics... fi [[ -n $VERBOSE ]] && VERBOSE="${VERBOSE}${key}\t${value}\t$warn\t$critical\t$min\t$max\n" done <<< "${AUREPORT}" # output: OK - metric=value ... echo -n "${STATUS}$shortmetrics" | tr -s ' ' if [[ -n ${VERBOSE} ]]; then if [[ -n $xmltable ]] ; then xmltable="xmltable" else xmltable="cat" fi echo ; # sort by nr of events echo -ne "$VERBOSE" | column -t -o ' ' | (read -r; printf "%s\n" "$REPLY"; sort -r -n -k 2 ) | ${xmltable} fi [[ -z ${NOMETRICS} ]] && echo "| $metrics" | sed -r -e 's/\;+\s/ /g' exit ${CODE}