Back to index

nagios-plugins  1.4.16
check_nmap.py
Go to the documentation of this file.
00001 #!/usr/bin/python
00002 # Change the above line if python is somewhere else
00003 
00004 #
00005 # check_nmap
00006 #  
00007 # Program: nmap plugin for Nagios
00008 # License: GPL
00009 # Copyright (c)  2000 Jacob Lundqvist (jaclu@galdrion.com)
00010 #
00011 _version_ = '1.21'
00012 #
00013 #
00014 # Description:
00015 #
00016 # Does a nmap scan, compares open ports to those given on command-line
00017 # Reports warning for closed that should be open and error for
00018 # open that should be closed.
00019 # If optional ports are given, no warning is given if they are closed
00020 # and they are included in the list of valid ports.
00021 #
00022 #   Requirements:
00023 #     python
00024 #     nmap
00025 #
00026 # History
00027 # -------
00028 # 1.21  2004-07-23 rippeld@hillsboroughcounty.org Updated parsing of nmap output to correctly identify closed ports
00029 # 1.20   2000-07-15 jaclu Updated params to correctly comply to plugin-standard
00030 #                         moved support classes to utils.py
00031 # 1.16   2000-07-14 jaclu made options and return codes more compatible with 
00032 #                         the plugin developer-guidelines
00033 # 1.15   2000-07-14 jaclu added random string to temp-file name
00034 # 1.14   2000-07-14 jaclu added check for error from subproc
00035 # 1.10   2000-07-14 jaclu converted main part to class
00036 # 1.08   2000-07-13 jaclu better param parsing
00037 # 1.07   2000-07-13 jaclu changed nmap param to -P0
00038 # 1.06   2000-07-13 jaclu make sure tmp file is deleted on errors
00039 # 1.05   2000-07-12 jaclu in debug mode, show exit code
00040 # 1.03   2000-07-12 jaclu error handling on nmap output
00041 # 1.01   2000-07-12 jaclu added license
00042 # 1.00   2000-07-12 jaclu implemented timeout handling
00043 # 0.20   2000-07-10 jaclu Initial release
00044 
00045 
00046 import sys, os, string, whrandom
00047 
00048 import tempfile
00049 from getopt import getopt
00050 
00051 #
00052 # import generic Nagios-plugin stuff
00053 #
00054 import utils
00055 
00056 # Where temp files should be placed
00057 tempfile.tempdir='/usr/local/nagios/var'
00058 
00059 # Base name for tempfile
00060 tempfile.template='check_nmap_tmp.'
00061 
00062 # location and possibly params for nmap
00063 nmap_cmd='/usr/bin/nmap -P0'
00064 
00065 
00066 
00067 
00068 
00069 
00070 #
00071 #  the class that does all the real work in this plugin...
00072 #
00073 # 
00074 class CheckNmap:
00075 
00076     # Retcodes, so we are compatible with nagios
00077     #ERROR=    -1
00078     UNKNOWN=  -1
00079     OK=        0
00080     WARNING=   1
00081     CRITICAL=  2
00082 
00083 
00084     def __init__(self,cmd_line=[]):
00085         """Constructor.
00086            arguments:
00087             cmd_line: normaly sys.argv[1:] if called as standalone program
00088        """
00089        self.tmp_file=''
00090        self.host=''       # host to check
00091        self.timeout=10    
00092        self.debug=0       # 1= show debug info
00093        self.ports=[]      # list of mandatory ports
00094        self.opt_ports=[]  # list of optional ports
00095        self.ranges=''     # port ranges for nmap
00096        self.exit_code=0   # numerical exit-code
00097        self.exit_msg=''   # message to caller
00098        
00099        self.ParseCmdLine(cmd_line)
00100        
00101     def Run(self):
00102         """Actually run the process.
00103            This method should be called exactly once.
00104        """
00105        
00106        #
00107        # Only call check_host if cmd line was accepted earlier
00108        #
00109        if self.exit_code==0:
00110            self.CheckHost()
00111 
00112        self.CleanUp()
00113        return self.exit_code,self.exit_msg
00114     
00115     def Version(self):
00116        return 'check_nmap %s' % _version_
00117     
00118     #-----------------------------------------
00119     #
00120     # class internal stuff below...
00121     #
00122     #-----------------------------------------
00123     
00124     #
00125     # Param checks
00126     #
00127     def param2int_list(self,s):
00128        lst=string.split(string.replace(s,',',' '))
00129        try:
00130            for i in range(len(lst)):
00131               lst[i]=int(lst[i])
00132        except:
00133            lst=[]
00134        return lst
00135            
00136     def ParseCmdLine(self,cmd_line):
00137        try:
00138            opt_list=getopt(cmd_line,'vH:ho:p:r:t:V',['debug','host=','help',
00139                'optional=','port=','range=','timeout','version'])
00140            for opt in opt_list[0]:
00141               if opt[0]=='-v' or opt[0]=='--debug':
00142                   self.debug=1
00143               elif opt[0]=='-H' or opt[0]=='--host':
00144                   self.host=opt[1]
00145               elif opt[0]=='-h' or opt[0]=='--help':
00146                   doc_help()
00147                   self.exit_code=1 # request termination
00148                   break
00149               elif opt[0]=='-o' or opt[0]=='--optional':
00150                   self.opt_ports=self.param2int_list(opt[1])
00151               elif opt[0]=='-p' or opt[0]=='--port':
00152                   self.ports=self.param2int_list(opt[1])
00153               elif opt[0]=='-r' or opt[0]=='--range':
00154                   r=string.replace(opt[1],':','-')
00155                   self.ranges=r
00156               elif opt[0]=='-t' or opt[0]=='--timeout':
00157                   self.timeout=opt[1]
00158               elif opt[0]=='-V' or opt[0]=='--version':
00159                   print self.Version()
00160                   self.exit_code=1 # request termination
00161                   break
00162               else:
00163                   self.host=''
00164                   break
00165 
00166        except:
00167            # unknown param
00168            self.host=''
00169            
00170        if self.debug:
00171            print 'Params:'
00172            print '-------'
00173            print 'host             = %s' % self.host
00174            print 'timeout          = %s' % self.timeout
00175            print 'ports            = %s' % self.ports
00176            print 'optional ports   = %s' % self.opt_ports
00177            print 'ranges           = %s' % self.ranges
00178            print
00179        
00180        #
00181        # a option that wishes us to terminate now has been given...
00182        # 
00183        # This way, you can test params in debug mode and see what this 
00184        # program recognised by suplying a version param at the end of
00185        # the cmd-line
00186        #
00187        if self.exit_code<>0:
00188            sys.exit(self.UNKNOWN)
00189            
00190        if self.host=='':
00191            doc_syntax()
00192            self.exit_code=self.UNKNOWN
00193            self.exit_msg='UNKNOWN: bad params, try running without any params for syntax'
00194                      
00195 
00196     def CheckHost(self):
00197        'Check one host using nmap.'
00198        #
00199        # Create a tmp file for storing nmap output
00200        #
00201        # The tempfile module from python 1.5.2 is stupid
00202        # two processes runing at aprox the same time gets 
00203        # the same tempfile...
00204        # For this reason I use a random suffix for the tmp-file
00205        # Still not 100% safe, but reduces the risk significally
00206        # I also inserted checks at various places, so that
00207        # _if_ two processes in deed get the same tmp-file
00208        # the only result is a normal error message to nagios
00209        #
00210        r=whrandom.whrandom()
00211        self.tmp_file=tempfile.mktemp('.%s')%r.randint(0,100000)
00212        if self.debug:
00213            print 'Tmpfile is: %s'%self.tmp_file
00214        #
00215        # If a range is given, only run nmap on this range
00216        #
00217        if self.ranges<>'':
00218            global nmap_cmd # needed, to avoid error on next line
00219                            # since we assigns to nmap_cmd :)
00220            nmap_cmd='%s -p %s' %(nmap_cmd,self.ranges)  
00221        #
00222        # Prepare a task
00223        #
00224        t=utils.Task('%s %s' %(nmap_cmd,self.host))
00225        #
00226        # Configure a time-out handler
00227        #
00228        th=utils.TimeoutHandler(t.Kill, time_to_live=self.timeout, 
00229                                debug=self.debug)
00230        #
00231        #  Fork of nmap cmd
00232        #
00233        t.Run(detach=0, stdout=self.tmp_file,stderr='/dev/null')
00234        #
00235        # Wait for completition, error or timeout
00236        #
00237        nmap_exit_code=t.Wait(idlefunc=th.Check, interval=1)
00238        #
00239        # Check for timeout
00240        #
00241        if th.WasTimeOut():
00242            self.exit_code=self.CRITICAL
00243            self.exit_msg='CRITICAL - Plugin timed out after %s seconds' % self.timeout
00244            return
00245        #
00246        # Check for exit status of subprocess
00247        # Must do this after check for timeout, since the subprocess
00248        # also returns error if aborted.
00249        #
00250        if nmap_exit_code <> 0:
00251            self.exit_code=self.UNKNOWN
00252            self.exit_msg='nmap program failed with code %s' % nmap_exit_code
00253            return
00254        #
00255        # Read output
00256        #
00257        try:
00258            f = open(self.tmp_file, 'r')
00259            output=f.readlines()
00260            f.close()
00261        except:
00262            self.exit_code=self.UNKNOWN
00263             self.exit_msg='Unable to get output from nmap'
00264            return
00265 
00266        #
00267        # Store open ports in list
00268        #  scans for lines where first word contains '/'
00269        #  and stores part before '/'
00270        #
00271        self.active_ports=[]
00272        try:
00273            for l in output:
00274               if len(l)<2:
00275                   continue
00276               s=string.split(l)[0]
00277               if string.find(s,'/')<1:
00278                   continue
00279               p=string.split(s,'/')[0]
00280               if string.find(l,'open')>1:
00281                   self.active_ports.append(int(p))
00282        except:
00283            # failure due to strange output...
00284            pass
00285 
00286        if self.debug:
00287            print 'Ports found by nmap:   ',self.active_ports
00288        #
00289        # Filter out optional ports, we don't check status for them...
00290        #
00291        try:
00292            for p in self.opt_ports:
00293               self.active_ports.remove(p)
00294            
00295            if self.debug and len(self.opt_ports)>0:
00296               print 'optional ports removed:',self.active_ports
00297        except:
00298            # under extreame loads the remove(p) above failed for me
00299            # a few times, this exception hanlder handles
00300            # this bug-alike situation...
00301            pass
00302 
00303        opened=self.CheckOpen()     
00304        closed=self.CheckClosed()
00305        
00306        if opened <>'':
00307            self.exit_code=self.CRITICAL
00308             self.exit_msg='PORTS CRITICAL - Open:%s Closed:%s'%(opened,closed)
00309        elif closed <>'':
00310            self.exit_code=self.WARNING
00311            self.exit_msg='PORTS WARNING - Closed:%s'%closed
00312        else:
00313            self.exit_code=self.OK
00314            self.exit_msg='PORTS ok - Only defined ports open'
00315     
00316     
00317     #
00318     # Compares requested ports on with actually open ports
00319     # returns all open that should be closed
00320     #
00321     def CheckOpen(self):
00322        opened=''
00323        for p in self.active_ports:
00324            if p not in self.ports:
00325               opened='%s %s' %(opened,p)
00326        return opened
00327        
00328     #
00329     # Compares requested ports with actually open ports
00330     # returns all ports that are should be open
00331     #
00332     def CheckClosed(self):
00333        closed=''
00334        for p in self.ports:
00335            if p not in self.active_ports:
00336               closed='%s %s' % (closed,p)
00337        return closed
00338 
00339 
00340     def CleanUp(self):
00341        #
00342        # If temp file exists, get rid of it
00343        #
00344        if self.tmp_file<>'' and os.path.isfile(self.tmp_file):
00345            try:
00346               os.remove(self.tmp_file)
00347            except:
00348               # temp-file colition, some other process already
00349               # removed the same file... 
00350               pass       
00351     
00352        #
00353        # Show numerical exits as string in debug mode
00354        #
00355        if self.debug:
00356            print 'Exitcode:',self.exit_code,
00357            if self.exit_code==self.UNKNOWN:
00358               print 'UNKNOWN'
00359            elif self.exit_code==self.OK:
00360               print 'OK'
00361            elif self.exit_code==self.WARNING:
00362               print 'WARNING'
00363            elif self.exit_code==self.CRITICAL:
00364               print 'CRITICAL'
00365            else:
00366               print 'undefined'
00367        #
00368        # Check if invalid exit code
00369        #
00370        if self.exit_code<-1 or self.exit_code>2:
00371            self.exit_msg=self.exit_msg+' - undefined exit code (%s)' % self.exit_code
00372            self.exit_code=self.UNKNOWN
00373 
00374 
00375         
00376 
00377 
00378 #
00379 # Help texts
00380 #
00381 def doc_head():
00382     print """
00383 check_nmap plugin for Nagios
00384 Copyright (c) 2000 Jacob Lundqvist (jaclu@galdrion.com)
00385 License: GPL
00386 Version: %s""" % _version_
00387     
00388 
00389 def doc_syntax():
00390     print """
00391 Usage: check_ports [-v|--debug] [-H|--host host] [-V|--version] [-h|--help]
00392                    [-o|--optional port1,port2,port3 ...] [-r|--range range]
00393                    [-p|--port port1,port2,port3 ...] [-t|--timeout timeout]"""
00394     
00395 
00396 def doc_help():
00397     'Help is displayed if run without params.'
00398     doc_head()
00399     doc_syntax()
00400     print """
00401 Options:
00402  -h         = help (this screen ;-)
00403  -v         = debug mode, show some extra output
00404  -H host    = host to check (name or IP#)
00405  -o ports   = optional ports that can be open (one or more),
00406              no warning is given if optional port is closed
00407  -p ports   = ports that should be open (one or more)
00408  -r range   = port range to feed to nmap.  Example: :1024,2049,3000:7000
00409  -t timeout = timeout in seconds, default 10
00410  -V         = Version info
00411  
00412 This plugin attempts to verify open ports on the specified host.
00413 
00414 If all specified ports are open, OK is returned.
00415 If any of them are closed, WARNING is returned (except for optional ports)
00416 If other ports are open, CRITICAL is returned
00417 
00418 If possible, supply an IP address for the host address, 
00419 as this will bypass the DNS lookup.        
00420 """
00421 
00422 
00423 #
00424 # Main
00425 #
00426 if __name__ == '__main__':
00427 
00428     if len (sys.argv) < 2:
00429        #
00430        # No params given, show syntax and exit
00431        #
00432        doc_syntax()
00433        sys.exit(-1)
00434        
00435     nmap=CheckNmap(sys.argv[1:])
00436     exit_code,exit_msg=nmap.Run()
00437     
00438     #
00439     # Give Nagios a msg and a code
00440     #
00441     print exit_msg
00442     sys.exit(exit_code)