Code

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