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
98 self.ParseCmdLine(cmd_line)
100 def Run(self):
101 """Actually run the process.
102 This method should be called exactly once.
103 """
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
114 def Version(self):
115 return 'check_nmap %s' % _version_
117 #-----------------------------------------
118 #
119 # class internal stuff below...
120 #
121 #-----------------------------------------
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
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=''
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
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)
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'
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)
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()
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'
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
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
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
376 #
377 # Help texts
378 #
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_
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]"""
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
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 """
421 #
422 # Main
423 #
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)
433 nmap=CheckNmap(sys.argv[1:])
434 exit_code,exit_msg=nmap.Run()
436 #
437 # Give Nagios a msg and a code
438 #
439 print exit_msg
440 sys.exit(exit_code)