#!/bin/sh
#
# check_ports: Nagios Plugin to monitor your FreeBSD Ports Tree for updates or
#              installed packages with known security vulnerabilities.
#
# http://code.adminlife.net/check_ports/
#
# Copyright (c) 2008-2009, Matthias Kellermann
# Copyright (c) 2014, 2017, Ryan Frederick
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright notice, this
#      list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
#

# please change locations appropriate
PKG_VERSION=/usr/sbin/pkg_version
PKG=/usr/sbin/pkg
JEXEC=/usr/sbin/jexec
JLS=/usr/sbin/jls
# TMP_PATH will be created by check_ports, needs to be writable
TMP_PATH=/tmp/jailaudit

###### global vars - don't touch ######

PROGNAME="${0##*/}"
VERSION=0.7.3
DATE="21 Jun 2017"
PROJECTURL=https://github.com/rfrederick/check_ports

RELEASE=$(uname -r)
RELEASE="${RELEASE%%.*}"
ANY_UPDATE=0
WARN_ON_ANY_UPDATE=0
CHECK_PORTS_AGE=0
UNPRIV_MODE=0
MSG_STATE=0
PORTSAGE=0
UPDATES=0
UPDATES_LIST=""
USE_PKG=0
PKGVERSION_OPTS="-Ivl "\<""
PKG_OPTS="version -Ivl "\<""

###### functions ######

# print version and exit
print_version() {
  printf "Nagios Plugin ${PROGNAME} for FreeBSD Ports v${VERSION} (${DATE})\n\n"
  printf ${PROJECTURL}"\n\n"
  exit 0
}

# print help msg and exit
print_help() {
  printf "usage: ${PROGNAME} [options] [-j jailname] [-I <path>] [-P <path>]\n\n"
  printf "options:\n"
  printf "  -h\t\tshow this help message and exit.\n\n"
  printf "  -a\t\tshows if any updates are available.\n"
  printf "\t\tDoes not change state, use -w for that.\n\n"
  printf "  -w\t\tshows if any updates are available.\n"
  printf "\t\tChanges state to warning.\n\n"
  printf "  -p\t\tdisplay warning if your ports tree is\n"
  printf "  \t\tolder than 24 hours.\n\n"
  printf "  -j jailname\tcheck jail instead of main system.\n\n"
  printf "  -c\t\tcheck environment for needed tools.\n"
  printf "  \t\tHighly recommended before first run.\n\n"
  printf "  -u \t\trun check_ports in unprivileged mode.\n"
  printf "  \t\tMuch slower but more secure.\n\n"
  printf "  -v\t\tshow version number.\n\n"
  printf "  -I <path>\tPath to INDEX file.\n\n"
  if [ ${RELEASE} -lt 10 ]
  then
    printf "  -P <path>\tPath to portaudit.\n\n"
    printf "  -g\t\tuse the pkg(8) suite of tools on FreeBSD <= 9.x\n"
    printf "  \t\timplicit on FreeBSD >= 10.x\n\n"
  fi
  exit 0
}

# print state to STDOUT with proper exit code for Nagios
print_state() {
  case ${2} in
    0) STATE=OK;;
    1) STATE=WARNING;;
    2) STATE=CRITICAL;;
    3) STATE=UNKNOWN;;
  esac
  printf "PORTS ${STATE} - ${1}\n"
  exit ${2}
}

# are we running FreeBSD?
check_os() {
  if [ $(uname -s) = "FreeBSD" ]
  then
    return 0
  else
    return 1
  fi
}

# portaudit accessible?
check_portaudit() {
  if [ -x ${PORTAUDIT} ]
  then
    return 0
  else
    return 1
  fi
}

# pkg_version accessible?
check_pkg_version() {
  if [ -x ${PKG_VERSION} ]
  then
    return 0
  else
    return 1
  fi
}

# pkg accessible?
check_pkg() {
  if [ -x ${PKG} ]
  then
    return 0
  else
    return 1
 fi
}

# jexec accessible?
check_jexec() {
  if [ -x ${JEXEC} ]
  then
    return 0
  else
    return 1
  fi
}

# portaudit database up to date?
check_portaudit_db() {
  ${PORTAUDIT} >/dev/null 2>&1
  if [ $? -eq 2 ]
  then
    return 1
  else
    return 0
  fi
}

# INDEX accessible?
check_portindex() {
  if [ -r ${PORTINDEX} ]
 then
   return 0
 else
   return 1
 fi
}

# pre check
check_env() {
  printf "checking environment ...\n\n"

  check_os
  if [ $? -eq 1 ]
  then
    printf "  ERROR: Your operating system is not FreeBSD.\n"
    printf "         This plugin only works under FreeBSD!\n"
  else
    printf "  OK:    Your operating system is FreeBSD, main release ${RELEASE}.\n"
  fi

  if [ ${USE_PKG} -eq 0 ]
  then
    check_portaudit
    if [ $? -eq 1 ]
    then
      printf "  ERROR: portaudit not found!\n"
    else
      printf "  OK:    portaudit found at ${PORTAUDIT}\n"
    fi

    check_pkg_version
    if [ $? -eq 1 ]
    then
      printf "  ERROR: pkg_version not found at ${PKG_VERSION}.\n"
      printf "         Update information will not be available!\n"
    else
      printf "  OK:    pkg_version found at ${PKG_VERSION}\n"
    fi

    check_portaudit_db
    PORTAUDIT_DB_DATE=$(${PORTAUDIT} -d | awk -F ": " '{print $2}')
    if [ $? -eq 1 ]
    then
      printf "  ERROR: portaudit database too old (Last Update: ${PORTAUDIT_DB_DATE}) - update with portaudit -F\n"
    else
      printf "  OK:    portaudit database is up to date (Last Update: ${PORTAUDIT_DB_DATE}).\n"
    fi
  else
    check_pkg
    if [ $? -eq 1 ]
    then
      printf "  ERROR: pkg not found at ${PKG}.\n"
      printf "         Update and security information will not be available!\n"
    else
      printf "  OK:    pkg found at ${PKG}\n"
    fi
  fi

  check_jexec
  if [ $? -eq 1 ]
  then
    printf "  ERROR: jexec not found.\n"
  else
    printf "  OK:    jexec found at ${JEXEC}\n"
  fi

  check_portindex
  if [ $? -eq 1 ]
  then
    printf "  ERROR: ${PORTINDEX} not readable.\n"
  else
    printf "  OK:    ${PORTINDEX} readable.\n"
  fi

  printf "\n"
  exit 0
}

# generate state mesage for Nagios and call print_state
run_gen_state() {
  MSG_PROBLEMS="${PROBLEMS} security problem(s)"

  if [ ${WARN_ON_ANY_UPDATE} -eq 1 -o ${ANY_UPDATE} -eq 1 ]
  then
    MSG_UPDATES=", ${UPDATES} Package(s) available for upgrade"
  fi

  if [ ${WARN_ON_ANY_UPDATE} -eq 1 -a ${UPDATES} -gt 0 ]
  then
    MSG_STATE="1"
    MSG_UPDATES=", ${UPDATES} Package(s) available for upgrade"
  fi

  if [ ${CHECK_PORTS_AGE} -eq 1 -a ${PORTSAGE} -gt 0 ]
  then
    MSG_STATE="1"
    MSG_PORTSAGE=", Ports Tree older than 24h"
  elif [ ${CHECK_PORTS_AGE} -eq 1 -a ${PORTSAGE} -eq 0 ]
  then
    MSG_PORTSAGE=", Ports Tree updated within the last 24h"
  fi

  MSG_PERFDATA="total_updates=${UPDATES};0;0 security_problems=${PROBLEMS};0;0"

  if [ -n "$UPDATES_LIST" ]
  then
    UPDATES_LIST="\n${UPDATES_LIST}"
  fi

  if [ ${PROBLEMS} -gt 0 ]
  then
    MSG_STATE="2"
  fi

  print_state "${MSG_PROBLEMS}${MSG_UPDATES}${MSG_PORTSAGE}. | ${MSG_PERFDATA}${UPDATES_LIST}" ${MSG_STATE}
}

# main function
run_main() {
  # count lines from portversion if asked
  if [ ${ANY_UPDATE} -eq 1 -o ${WARN_ON_ANY_UPDATE} -eq 1  ]
  then
    if [ ${USE_PKG} -eq 0 ]
    then
      UPDATES_LIST=$(${PKG_VERSION} ${PKGVERSION_OPTS} ${PORTINDEX} | grep "needs updating" | awk '{ print $1 " " $5 " " $6 " " $7 }')
      UPDATES=$(echo "${UPDATES_LIST}" | grep -c .)
    else
      UPDATES_LIST=$(${PKG} ${PKG_OPTS} ${PORTINDEX} | grep "needs updating" | awk '{ print $1 " " $5 " " $6 " " $7 }')
      UPDATES=$(echo "${UPDATES_LIST}" | grep -c .)
    fi
  fi
  
  # count lines from find
  if [ ${CHECK_PORTS_AGE} -eq 1 ]
  then
    PORTSAGE=$(find ${PORTINDEX} -name ${PORTINDEX##*/} -mtime +1 | grep -c ${PORTINDEX##*/})
  fi

  # count lines from portaudit
  if [ ${USE_PKG} -eq 0 ]
  then
    PROBLEMS=$(${PORTAUDIT} | egrep "problem\(s\) in (your|the|[0-9]{1,}) (installed )?(packages|package\(s\)) found\." | awk '{ print $1 }')
  else
    PROBLEMS=$(${PKG} audit | egrep "problem\(s\) in (your|the|[0-9]{1,}) (installed )?(packages|package\(s\)) found\." | awk '{ print $1 }')
  fi

  run_gen_state
}

# main function for use in jails
run_main_jail() {
  if [ ${UNPRIV_MODE} -eq 1 -a ${USE_PKG} -eq 0 ]
  then
    mkdir ${TMP_PATH} >/dev/null 2>&1
    ls -1 ${JAIL_PATH}/var/db/pkg/ > ${TMP_PATH}/${JAIL} 2>/dev/null
    # count lines from portaudit
    PROBLEMS=$(${PORTAUDIT} -f ${TMP_PATH}/${JAIL} | grep "problem(s) found." | awk '{ print $1 }')
    rm ${TMP_PATH}/${JAIL}
    rmdir ${TMP_PATH} >/dev/null 2>&1
  else
    if [ $(id -ru) -ne 0 ]
    then
      print_state "only root can execute jail checks - users should use -u mode" "3"
    else
      # count lines from portaudit
      if [ ${USE_PKG} -eq 0 ]
      then
        PROBLEMS=$(${JEXEC} ${JID}  ${PORTAUDIT} | egrep "problem\(s\) in (your|the|[0-9]{1,}) (installed )?(packages|package\(s\)) found\." | awk '{ print $1 }')
      else
        PROBLEMS=$(${PKG} -j ${JID} audit | egrep "problem\(s\) in (your|the|[0-9]{1,}) (installed )?(packages|package\(s\)) found\." | awk '{ print $1 }')
      fi
    fi
  fi
  # count lines from pkg_version if asked and set PKG_DBDIR
  if [ ${ANY_UPDATE} -eq 1 -o ${WARN_ON_ANY_UPDATE} -eq 1  ]
  then
    if [ ${USE_PKG} -eq 0 ]
    then
      UPDATES_LIST=$(PKG_DBDIR=${JAIL_PATH}/var/db/pkg ${PKG_VERSION} ${PKGVERSION_OPTS} ${JAIL_PATH}${PORTINDEX} | grep "needs updating" | awk '{ print $1 " " $5 " " $6 " " $7 }')
      UPDATES=$(echo "${UPDATES_LIST}" | grep -c .)
    else
      UPDATES_LIST=$(${PKG} -j ${JID} ${PKG_OPTS} ${PORTINDEX} | grep "needs updating" | awk '{ print $1 " " $5 " " $6 " " $7 }')
      UPDATES=$(echo "${UPDATES_LIST}" | grep -c .)
    fi
  fi

  # count lines from find
  if [ ${CHECK_PORTS_AGE} -eq 1 ]
  then
    PORTSAGE=$(find ${JAIL_PATH}${PORTINDEX} -name ${PORTINDEX##*/} -mtime +1 | grep -c ${PORTINDEX##*/})
  fi

  run_gen_state
}

###### main ######

while getopts I:P:hvcawpuj:g opt 2>/dev/null
do
  case $opt in
    I) PORTINDEX="$OPTARG" ;;
    P) PORTAUDIT="$OPTARG" ;;
    h) print_help;;
    v) print_version;;
    c) run_check_env=run_check_env;;
    a) ANY_UPDATE=1;;
    w) WARN_ON_ANY_UPDATE=1;;
    p) CHECK_PORTS_AGE=1;;
    u) UNPRIV_MODE=1;;
    j) JAIL=${OPTARG};;
    g) USE_PKG=1;;
    ?) exit 1;;
  esac
done

# check OS release, use the pkg(8) suite of tools implicitly if the release is 10 or greater
if [ ${RELEASE} -ge 10 ]
then
  USE_PKG=1
fi

PORTDIR=/usr/ports
: ${PORTINDEX:="${PORTDIR}/INDEX-${RELEASE}"}
if [ ${USE_PKG} -eq 0 ]
then
  : ${PORTAUDIT:=/usr/local/sbin/portaudit}
fi

[ -n "$run_check_env" ] && check_env

# check for improper usage
if [ ${WARN_ON_ANY_UPDATE} -eq 1 -a ${ANY_UPDATE} -eq 1 ]
then
  print_state "options -a and -w can not be used together." "3"
fi

# check for jail, then run run_main() or run_main_jail()
if [ -z ${JAIL} ]
then
  if [ ${UNPRIV_MODE} -eq 1 ]
  then
    print_state "option -u can only be used with a jail." "3"
  fi
  run_main
else
  if [ ${USE_PKG} -eq 1 -a $UNPRIV_MODE -eq 1 ]
  then
    print_state "option -u cannot be used with the pkg(8) suite of tools." "3"
  fi
  if [ ! -s /var/run/jail_${JAIL}.id ]
  then
    print_state "Jail ${JAIL} not found (is ${JAIL} running?)." "3"
  elif [ ! -r /var/run/jail_${JAIL}.id ]
  then
    print_state "Jail ${JAIL} not readable." "3"
  else
    JID=$(cat /var/run/jail_${JAIL}.id)
    JAIL_PATH=$(${JLS} | awk -v jid="${JID}" '$1 == jid {print $4}') 
    run_main_jail
  fi
fi
