Code

Fix for regex input of '|', being output causing problems with Nagios' parsing of
[nagiosplug.git] / contrib / check_nmap.py
1 #!/usr/bin/python
2 # Change the above line if python is somewhere else
4 #
5 # check_nmap
6 #  
7 # Program: nmap plugin for Nagios
8 # License: GPL
9 # Copyright (c)  2000 Jacob Lundqvist (jaclu@galdrion.com)
10 #
11 _version_ = '1.21'
12 #
13 #
14 # Description:
15 #
16 # Does a nmap scan, compares open ports to those given on command-line
17 # Reports warning for closed that should be open and error for
18 # open that should be closed.
19 # If optional ports are given, no warning is given if they are closed
20 # and they are included in the list of valid ports.
21 #
22 #   Requirements:
23 #     python
24 #     nmap
25 #
26 # History
27 # -------
28 # 1.21   2004-07-23 rippeld@hillsboroughcounty.org Updated parsing of nmap output to correctly identify closed ports
29 # 1.20   2000-07-15 jaclu Updated params to correctly comply to plugin-standard
30 #                         moved support classes to utils.py
31 # 1.16   2000-07-14 jaclu made options and return codes more compatible with 
32 #                         the plugin developer-guidelines
33 # 1.15   2000-07-14 jaclu added random string to temp-file name
34 # 1.14   2000-07-14 jaclu added check for error from subproc
35 # 1.10   2000-07-14 jaclu converted main part to class
36 # 1.08   2000-07-13 jaclu better param parsing
37 # 1.07   2000-07-13 jaclu changed nmap param to -P0
38 # 1.06   2000-07-13 jaclu make sure tmp file is deleted on errors
39 # 1.05   2000-07-12 jaclu in debug mode, show exit code
40 # 1.03   2000-07-12 jaclu error handling on nmap output
41 # 1.01   2000-07-12 jaclu added license
42 # 1.00   2000-07-12 jaclu implemented timeout handling
43 # 0.20   2000-07-10 jaclu Initial release
46 import sys, os, string, whrandom
48 import tempfile
49 from getopt import getopt
51 #
52 # import generic Nagios-plugin stuff
53 #
54 import utils
56 # Where temp files should be placed
57 tempfile.tempdir='/usr/local/nagios/var'
59 # Base name for tempfile
60 tempfile.template='check_nmap_tmp.'
62 # location and possibly params for nmap
63 nmap_cmd='/usr/bin/nmap -P0'
70 #
71 #  the class that does all the real work in this plugin...
72 #
73
74 class CheckNmap:
76     # Retcodes, so we are compatible with nagios
77     #ERROR=    -1
78     UNKNOWN=  -1
79     OK=        0
80     WARNING=   1
81     CRITICAL=  2
84     def __init__(self,cmd_line=[]):
85         """Constructor.
86            arguments:
87              cmd_line: normaly sys.argv[1:] if called as standalone program
88         """
89         self.tmp_file=''
90         self.host=''       # host to check
91         self.timeout=10    
92         self.debug=0       # 1= show debug info
93         self.ports=[]      # list of mandatory ports
94         self.opt_ports=[]  # list of optional ports
95         self.ranges=''     # port ranges for nmap
96         self.exit_code=0   # numerical exit-code
97         self.exit_msg=''   # message to caller
98         
99         self.ParseCmdLine(cmd_line)
100         
101     def Run(self):
102         """Actually run the process.
103            This method should be called exactly once.
104         """
105         
106         #
107         # Only call check_host if cmd line was accepted earlier
108         #
109         if self.exit_code==0:
110             self.CheckHost()
112         self.CleanUp()
113         return self.exit_code,self.exit_msg
114     
115     def Version(self):
116         return 'check_nmap %s' % _version_
117     
118     #-----------------------------------------
119     #
120     # class internal stuff below...
121     #
122     #-----------------------------------------
123     
124     #
125     # Param checks
126     #
127     def param2int_list(self,s):
128         lst=string.split(string.replace(s,',',' '))
129         try:
130             for i in range(len(lst)):
131                 lst[i]=int(lst[i])
132         except:
133             lst=[]
134         return lst
135             
136     def ParseCmdLine(self,cmd_line):
137         try:
138             opt_list=getopt(cmd_line,'vH:ho:p:r:t:V',['debug','host=','help',
139                 'optional=','port=','range=','timeout','version'])
140             for opt in opt_list[0]:
141                 if opt[0]=='-v' or opt[0]=='--debug':
142                     self.debug=1
143                 elif opt[0]=='-H' or opt[0]=='--host':
144                     self.host=opt[1]
145                 elif opt[0]=='-h' or opt[0]=='--help':
146                     doc_help()
147                     self.exit_code=1 # request termination
148                     break
149                 elif opt[0]=='-o' or opt[0]=='--optional':
150                     self.opt_ports=self.param2int_list(opt[1])
151                 elif opt[0]=='-p' or opt[0]=='--port':
152                     self.ports=self.param2int_list(opt[1])
153                 elif opt[0]=='-r' or opt[0]=='--range':
154                     r=string.replace(opt[1],':','-')
155                     self.ranges=r
156                 elif opt[0]=='-t' or opt[0]=='--timeout':
157                     self.timeout=opt[1]
158                 elif opt[0]=='-V' or opt[0]=='--version':
159                     print self.Version()
160                     self.exit_code=1 # request termination
161                     break
162                 else:
163                     self.host=''
164                     break
166         except:
167             # unknown param
168             self.host=''
169             
170         if self.debug:
171             print 'Params:'
172             print '-------'
173             print 'host             = %s' % self.host
174             print 'timeout          = %s' % self.timeout
175             print 'ports            = %s' % self.ports
176             print 'optional ports   = %s' % self.opt_ports
177             print 'ranges           = %s' % self.ranges
178             print
179         
180         #
181         # a option that wishes us to terminate now has been given...
182         # 
183         # This way, you can test params in debug mode and see what this 
184         # program recognised by suplying a version param at the end of
185         # the cmd-line
186         #
187         if self.exit_code<>0:
188             sys.exit(self.UNKNOWN)
189             
190         if self.host=='':
191             doc_syntax()
192             self.exit_code=self.UNKNOWN
193             self.exit_msg='UNKNOWN: bad params, try running without any params for syntax'
194                 
196     def CheckHost(self):
197         'Check one host using nmap.'
198         #
199         # Create a tmp file for storing nmap output
200         #
201         # The tempfile module from python 1.5.2 is stupid
202         # two processes runing at aprox the same time gets 
203         # the same tempfile...
204         # For this reason I use a random suffix for the tmp-file
205         # Still not 100% safe, but reduces the risk significally
206         # I also inserted checks at various places, so that
207         # _if_ two processes in deed get the same tmp-file
208         # the only result is a normal error message to nagios
209         #
210         r=whrandom.whrandom()
211         self.tmp_file=tempfile.mktemp('.%s')%r.randint(0,100000)
212         if self.debug:
213             print 'Tmpfile is: %s'%self.tmp_file
214         #
215         # If a range is given, only run nmap on this range
216         #
217         if self.ranges<>'':
218             global nmap_cmd # needed, to avoid error on next line
219                             # since we assigns to nmap_cmd :)
220             nmap_cmd='%s -p %s' %(nmap_cmd,self.ranges) 
221         #
222         # Prepare a task
223         #
224         t=utils.Task('%s %s' %(nmap_cmd,self.host))
225         #
226         # Configure a time-out handler
227         #
228         th=utils.TimeoutHandler(t.Kill, time_to_live=self.timeout, 
229                                 debug=self.debug)
230         #
231         #  Fork of nmap cmd
232         #
233         t.Run(detach=0, stdout=self.tmp_file,stderr='/dev/null')
234         #
235         # Wait for completition, error or timeout
236         #
237         nmap_exit_code=t.Wait(idlefunc=th.Check, interval=1)
238         #
239         # Check for timeout
240         #
241         if th.WasTimeOut():
242             self.exit_code=self.CRITICAL
243             self.exit_msg='CRITICAL - Plugin timed out after %s seconds' % self.timeout
244             return
245         #
246         # Check for exit status of subprocess
247         # Must do this after check for timeout, since the subprocess
248         # also returns error if aborted.
249         #
250         if nmap_exit_code <> 0:
251             self.exit_code=self.UNKNOWN
252             self.exit_msg='nmap program failed with code %s' % nmap_exit_code
253             return
254         #
255         # Read output
256         #
257         try:
258             f = open(self.tmp_file, 'r')
259             output=f.readlines()
260             f.close()
261         except:
262             self.exit_code=self.UNKNOWN
263             self.exit_msg='Unable to get output from nmap'
264             return
266         #
267         # Store open ports in list
268         #  scans for lines where first word contains '/'
269         #  and stores part before '/'
270         #
271         self.active_ports=[]
272         try:
273             for l in output:
274                 if len(l)<2:
275                     continue
276                 s=string.split(l)[0]
277                 if string.find(s,'/')<1:
278                     continue
279                 p=string.split(s,'/')[0]
280                 if string.find(l,'open')>1:
281                     self.active_ports.append(int(p))
282         except:
283             # failure due to strange output...
284             pass
286         if self.debug:
287             print 'Ports found by nmap:   ',self.active_ports
288         #
289         # Filter out optional ports, we don't check status for them...
290         #
291         try:
292             for p in self.opt_ports:
293                 self.active_ports.remove(p)
294             
295             if self.debug and len(self.opt_ports)>0:
296                 print 'optional ports removed:',self.active_ports
297         except:
298             # under extreame loads the remove(p) above failed for me
299             # a few times, this exception hanlder handles
300             # this bug-alike situation...
301             pass
303         opened=self.CheckOpen() 
304         closed=self.CheckClosed()
305         
306         if opened <>'':
307             self.exit_code=self.CRITICAL
308             self.exit_msg='PORTS CRITICAL - Open:%s Closed:%s'%(opened,closed)
309         elif closed <>'':
310             self.exit_code=self.WARNING
311             self.exit_msg='PORTS WARNING - Closed:%s'%closed
312         else:
313             self.exit_code=self.OK
314             self.exit_msg='PORTS ok - Only defined ports open'
315     
316     
317     #
318     # Compares requested ports on with actually open ports
319     # returns all open that should be closed
320     #
321     def CheckOpen(self):
322         opened=''
323         for p in self.active_ports:
324             if p not in self.ports:
325                 opened='%s %s' %(opened,p)
326         return opened
327         
328     #
329     # Compares requested ports with actually open ports
330     # returns all ports that are should be open
331     #
332     def CheckClosed(self):
333         closed=''
334         for p in self.ports:
335             if p not in self.active_ports:
336                 closed='%s %s' % (closed,p)
337         return closed
340     def CleanUp(self):
341         #
342         # If temp file exists, get rid of it
343         #
344         if self.tmp_file<>'' and os.path.isfile(self.tmp_file):
345             try:
346                 os.remove(self.tmp_file)
347             except:
348                 # temp-file colition, some other process already
349                 # removed the same file... 
350                 pass        
351     
352         #
353         # Show numerical exits as string in debug mode
354         #
355         if self.debug:
356             print 'Exitcode:',self.exit_code,
357             if self.exit_code==self.UNKNOWN:
358                 print 'UNKNOWN'
359             elif self.exit_code==self.OK:
360                 print 'OK'
361             elif self.exit_code==self.WARNING:
362                 print 'WARNING'
363             elif self.exit_code==self.CRITICAL:
364                 print 'CRITICAL'
365             else:
366                 print 'undefined'
367         #
368         # Check if invalid exit code
369         #
370         if self.exit_code<-1 or self.exit_code>2:
371             self.exit_msg=self.exit_msg+' - undefined exit code (%s)' % self.exit_code
372             self.exit_code=self.UNKNOWN
375         
379 # Help texts
381 def doc_head():
382     print """
383 check_nmap plugin for Nagios
384 Copyright (c) 2000 Jacob Lundqvist (jaclu@galdrion.com)
385 License: GPL
386 Version: %s""" % _version_
387     
389 def doc_syntax():
390     print """
391 Usage: check_ports [-v|--debug] [-H|--host host] [-V|--version] [-h|--help]
392                    [-o|--optional port1,port2,port3 ...] [-r|--range range]
393                    [-p|--port port1,port2,port3 ...] [-t|--timeout timeout]"""
394     
396 def doc_help():
397     'Help is displayed if run without params.'
398     doc_head()
399     doc_syntax()
400     print """
401 Options:
402  -h         = help (this screen ;-)
403  -v         = debug mode, show some extra output
404  -H host    = host to check (name or IP#)
405  -o ports   = optional ports that can be open (one or more),
406               no warning is given if optional port is closed
407  -p ports   = ports that should be open (one or more)
408  -r range   = port range to feed to nmap.  Example: :1024,2049,3000:7000
409  -t timeout = timeout in seconds, default 10
410  -V         = Version info
411  
412 This plugin attempts to verify open ports on the specified host.
414 If all specified ports are open, OK is returned.
415 If any of them are closed, WARNING is returned (except for optional ports)
416 If other ports are open, CRITICAL is returned
418 If possible, supply an IP address for the host address, 
419 as this will bypass the DNS lookup.        
420 """
424 # Main
426 if __name__ == '__main__':
428     if len (sys.argv) < 2:
429         #
430         # No params given, show syntax and exit
431         #
432         doc_syntax()
433         sys.exit(-1)
434         
435     nmap=CheckNmap(sys.argv[1:])
436     exit_code,exit_msg=nmap.Run()
437     
438     #
439     # Give Nagios a msg and a code
440     #
441     print exit_msg
442     sys.exit(exit_code)