#!/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 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,--maxcheckpointage Max age in seconds after wich checkpoint file is overriden with 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 -h,--help Show this message --[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 -s' --lost= Number of lost audit events --backlog= Number of audit events in backlog -X Output xmltable if using '-v' 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:-760}" ; shift 2 ; 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} if [[ -f $CHECKPOINTFILE && -w $CHECKPOINTFILE && -z $NOCHECKPOINT ]] ; 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 -ts checkpoint --checkpoint ${CHECKPOINTFILE}" else 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 -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 -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 -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} --raw ${AUSEARCHARGS} | $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 # 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 $CODE fi fi # 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 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 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 =~ '!' ]] ; then shortmetrics="$key=${value}${sign} ${shortmetrics}" # show after status OK - shortmetrics... else [[ $value -gt 0 ]] && 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 [[ -n $xml ]] && xml="| xmltable" echo ; # sort by nr of events echo -ne "$VERBOSE" | column -t | (read -r; printf "%s\n" "$REPLY"; sort -r -n -k 2) echo -ne "checkpointfileage\t$CHECKPOINTAGE\t0\t0\t0\t$MAXCHECKPOINTAGE\n" fi [[ -z ${NOMETRICS} ]] && echo "| $metrics" | tr -s '; ' exit ${CODE}