#! /usr/bin/perl -w
################################################################################
# check_snmp_patchlevel.pl v1.2
#
# Nagios plugin to check the OS version string through SNMP sysDescr value.
# Please adjust the line setting the "use lib" path so utils.pm can be found.
# @2009 public[at]frank4dd[dot]com
#
# updates:
# 20090228 v1.1 adds support for Cisco PIX
# 20091117 v1.2 adds support for selection of SNMP version 1 or 2
################################################################################

use strict;
use Getopt::Long;
use Net::SNMP;

use vars qw($opt_V $opt_v $opt_h $opt_H $opt_g $opt_C $opt_f $PROGNAME @raw_data $output);
use lib "/usr/local/opmon/libexec"  ;
use utils qw(%ERRORS &print_revision &support &usage);

$PROGNAME = "check_snmp_patchlevel.pl";

sub print_help ();
sub print_usage ();

$ENV{'PATH'}='';
$ENV{'BASH_ENV'}=''; 
$ENV{'ENV'}='';

#print("Args: $#ARGV\n");
if ($#ARGV == -1) { usage("Missing arguments\.\nUsage: $PROGNAME -H <host> [-v snmp version 1|2] -g <ios|asa|pix> [-C community] [-f <config file>]\n"); }

Getopt::Long::Configure('bundling');
GetOptions
        ("V"   => \$opt_V, "plugin-version"=> \$opt_V,
         "v=s" => \$opt_v, "snmp-version=s"=> \$opt_v,
         "h"   => \$opt_h, "help"          => \$opt_h,
         "H=s" => \$opt_H, "hostname=s"    => \$opt_H,
         "C=s" => \$opt_C, "community=s"   => \$opt_C,
         "f=s" => \$opt_f, "configfile=s"  => \$opt_f,
         "g=s" => \$opt_g, "devicegroup=s" => \$opt_g);

if ($opt_V) {
	print_revision($PROGNAME,'$Revision: 120 $');
	exit $ERRORS{'OK'};
}

if ($opt_h) { print_help(); }

# The SNMP port defaults to 161. It is not made a commandline option yet.
my $opt_p = 161;
# The SNMP timeout is set to 5 seconds. It is not made a commandline option yet.
my $opt_t = 5;

################################################################################
# Check for the "required" options -H <host name or IP>, -g <ios|asa|pix>
################################################################################

($opt_H) || usage("Host name/address not specified\. Use $PROGNAME -h for help\.\n");
my $host = $1 if ($opt_H =~ /([-.A-Za-z0-9]+)/);
($host) || usage("Invalid host: $opt_H\n");

($opt_g) || usage("Device type not specified\. Use $PROGNAME -h for help\.\n");
# no sanity chck here, it is done below where $opt_g is parsed.

################################################################################
# Check for the "optional" options -v -C, -f, -s
################################################################################
# set the SNMP version or assume it is "1" (default)
($opt_v) || ($opt_v = 1) ;
if ( $opt_v ne 1 && $opt_v ne 2) {
  printf("UNKNOWN: SNMP version $opt_v.\n");
  exit $ERRORS{'UNKNOWN'};
}

# set the SNMP community or assume it is "public" (default)
($opt_C) || ($opt_C = "public") ;

# load config file
if($opt_f) { &read_config(); }

################################################################################
# We fetch the system description via SNMP. It should contain the OS version.
# The SNMP OID we query is .1.3.6.1.2.1.1.1.0 = SNMPv2-MIB::sysDescr.0
# $version = `/usr/bin/snmpget -OQv -v 1 -c $opt_C $host .1.3.6.1.2.1.1.1.0`;
################################################################################
my $snmpdata="";
my $response="";
my $sysdesc_oid = ".1.3.6.1.2.1.1.1.0";

my ($session, $error) = Net::SNMP->session(
  -hostname  => $opt_H,
  -community => $opt_C,
  -port      => $opt_p,
  -timeout   => $opt_t,
  -version   => $opt_v
);

if(!defined($session)) { printf("UNKNOWN: %s.\n", $error); exit $ERRORS{'UNKNOWN'}; }

$response = $session->get_request( -varbindlist => [$sysdesc_oid]);

if(!defined($response)) { 
  printf("UNKNOWN: %s.\n", $session->error); 
  $session->close;
  exit $ERRORS{'UNKNOWN'};
}

$snmpdata = $response->{$sysdesc_oid};
$session->close;

################################################################################
# There is no standardized way for displaying OS version and patch levels.
# Depending on the vendor and OS, we need to parse this data differently.
# We don't have a switch statement yet (coming in perl 5.10), so we need
# to cascade the if-thens
################################################################################

################################################################################
# Example response for Cisco Routers, here a from a Cat-6506:
# Version: Cisco Internetwork Operating System Software
# IOS (tm) MSFC2 Software (C6MSFC2-JSV-M), Version 12.1(27b)E3, RELEASE SOFTWARE (fc1)
#                                                  ^^^^^^^^^^^
# Technical Support: http://www.cisco.com/techsupport
# Copyright (c) 1986-2007 by cisco Systems, Inc.
# Compiled Tue 07-Aug-0
# Example response for Cisco Routers, here a from a Cat-3750:
# Cisco IOS Software, C3750 Software (C3750-IPBASE-M), Version 12.2(25)SEE2, RELEASE SOFTWARE (fc1)
#                                                              ^^^^^^^^^^^^
# Copyright (c) 1986-2006 by Cisco Systems, Inc.
# Compiled Fri 28-Jul-06 08:46 by yenanh
################################################################################
my $version="";

if ($opt_g =~ /ios/) { 
  my $line="";
  my @lines = split ('\n', $snmpdata);
  foreach $line (@lines) {
    if($line =~ /IOS .*/) { 
      (my @fields) = split(', ', $line);
      foreach my $field (@fields) {
        if($field =~ /Version/) { (my $txt, $version) = split('Version ', $field); }
      }
    }
  }
}

################################################################################
# Example response for Cisco ASA 5520:
# "Cisco Adaptive Security Appliance Version 8.0(4)"
#                                            ^^^^^^
################################################################################
elsif ($opt_g =~ /asa/) {
  if($snmpdata =~ /^Cisco Adaptive Security Appliance.*/) {
    (my $os, $version) = split('Version ', $snmpdata);
  }
}

###############################################################################
# Example response for Cisco PIX 525:
# "Cisco PIX Firewall Version 6.3(5)"
# "Cisco Cisco PIX Security Appliance Version 7.2(2)"
# "Cisco Cisco PIX Security Appliance Version 8.0(2)"
###############################################################################
elsif ($opt_g =~ /pix/) {
  if($snmpdata =~ /.* PIX .*/) {
    (my $os, $version) = split('Version ', $snmpdata);
  }
}

else { usage("Unknown option parameter: $opt_g\. Use $PROGNAME -h for help\.\n"); }

################################################################################
# If the SNMP data cannot be parsed, we generate the exit code and finish.
################################################################################
if($version eq "") {
  printf("UNKNOWN: cannot find version string in SNMP response. Either -t <type> is incorrect or the Version string is unreadable.\n");
  exit $ERRORS{'UNKNOWN'};
}
################################################################################
# We are in 'discovery' mode, we report the OS Version, return 'OK' and finish.
################################################################################
elsif (! $opt_f) {
  printf (uc($opt_g)." Version: $version | $snmpdata\n");
  exit $ERRORS{'OK'};
}
################################################################################
# We are in 'compliance' mode, we check the OS Version against the config file
################################################################################
else {
  foreach my $line (@raw_data) {
    # skip comment lines
    next if($line =~ /^#.*$/);
    chomp($line);

    (my $required, my $osgroup, my $osversion, my $remarks)=split(/\|/,$line);

    if( ($opt_g eq $osgroup) && ($version eq $osversion) ) {

      if($required eq "approved") {
        $output = uc($opt_g)." Version: $version approved";
        if ($remarks ne "") { $output = $output." | Remarks: ".$remarks." Data: $snmpdata\n"; }
        else { $output = $output." | $snmpdata\n"; }
        printf $output; exit $ERRORS{'OK'};
      }

      if($required eq "obsolete") {
        $output = uc($opt_g)." Version: $version obsolete";
        if ($remarks ne "") { $output = $output." | Remarks: ".$remarks." Data: $snmpdata\n"; }
        else { $output = $output." | $snmpdata\n"; }
        printf $output; exit $ERRORS{'WARNING'};
      }

      if($required eq "med-vuln") {
        $output = uc($opt_g)." Version: $version vulnerable (low-medium)";
        if ($remarks ne "") { $output = $output." | Remarks: ".$remarks." Data: $snmpdata\n"; }
        else { $output = $output." | $snmpdata\n"; }
        printf $output; exit $ERRORS{'WARNING'};
      }

      if($required eq "crit-vuln") {
        $output = uc($opt_g)." Version: $version vulnerable (high risk)";
        if ($remarks ne "") { $output = $output." | Remarks: ".$remarks." Data: $snmpdata\n"; }
        else { $output = $output." | $snmpdata\n"; }
        printf $output; exit $ERRORS{'CRITICAL'};
      }
    }
  }
  # the OS version is not listed, we don't know exactly if its good or bad.
  printf (uc($opt_g)." Version: $version unverified | $snmpdata\n");
  exit $ERRORS{'UNKNOWN'};
}
################################################################################
# End of main. Subroutine definitions below.
################################################################################
sub read_config () {

  if (! -e $opt_f) { usage("Cannot find file $opt_f\. Check file name and path\.\n"); }
  open(DATFILE, $opt_f) || usage("Cannot read file $opt_f\. Check permissions\.\n");
  @raw_data=<DATFILE>;
  close(DATFILE);
}

sub print_usage () {
  print "Usage: $PROGNAME -H <host> [-v snmp version 1|2] -g <ios|asa|pix> [-C community] [-f <config file>]\n";
  exit $ERRORS{'OK'};
}

sub print_help () {
  print_revision($PROGNAME,'$Revision: 1.0.0 $');
  print "Copyright (c) 2009 Frank4DD

This plugin intends to report the OS version string of a supported vendor.
Currently it is parsing Cisco IOS, PIX and ASA versions through SNMP sysDescr polls.

In 'discovery mode' without using the option '-f', this plugin returns the OS
version string with 'OK' if the version could be fetched, or 'UNKNOWN' if not.
Useful if you need a view on what is out there in a enterprise environment.

In 'compliance mode' by specifying '-f', this plugin compares the device OS
version string against a list of categorized (approved) OS versions. It is
meant to identify devices running a rogue, obsolete or vulnerable OS version.

Usually this check runs only once in a couple of hours or even longer periods.
The configuration file format is in the check_snmp_patchlevel.cfg template.

";
	print_usage();
	print "
-H, --hostname=HOST
   Name or IP address of host to check
-v, --snmp-version [1|2]
   Specify the SNMP version to use: 1 or 2c
-g, --devicegroup=[ios|asa]
   OS version string to expect: ios = Cisco IOS Routers
				asa = Cisco ASA Appliances
                                pix = Cisco PIX Firewalls
-C, --community=community
   SNMP community (default public)
-f, --configfile=STRING
   Version file and path, contains a list of approved versions

";
  support();
  exit $ERRORS{'OK'};
}
