diff options
Diffstat (limited to 'contrib/check_nmap.py')
-rw-r--r-- | contrib/check_nmap.py | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/contrib/check_nmap.py b/contrib/check_nmap.py new file mode 100644 index 00000000..4f53406d --- /dev/null +++ b/contrib/check_nmap.py | |||
@@ -0,0 +1,440 @@ | |||
1 | #!/usr/bin/python | ||
2 | # Change the above line if python is somewhere else | ||
3 | |||
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 | ||
43 | |||
44 | |||
45 | import sys, os, string, whrandom | ||
46 | |||
47 | import tempfile | ||
48 | from getopt import getopt | ||
49 | |||
50 | # | ||
51 | # import generic Nagios-plugin stuff | ||
52 | # | ||
53 | import utils | ||
54 | |||
55 | # Where temp files should be placed | ||
56 | tempfile.tempdir='/usr/local/nagios/var' | ||
57 | |||
58 | # Base name for tempfile | ||
59 | tempfile.template='check_nmap_tmp.' | ||
60 | |||
61 | # location and possibly params for nmap | ||
62 | nmap_cmd='/usr/bin/nmap -P0' | ||
63 | |||
64 | |||
65 | |||
66 | |||
67 | |||
68 | |||
69 | # | ||
70 | # the class that does all the real work in this plugin... | ||
71 | # | ||
72 | # | ||
73 | class CheckNmap: | ||
74 | |||
75 | # Retcodes, so we are compatible with nagios | ||
76 | #ERROR= -1 | ||
77 | UNKNOWN= -1 | ||
78 | OK= 0 | ||
79 | WARNING= 1 | ||
80 | CRITICAL= 2 | ||
81 | |||
82 | |||
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() | ||
110 | |||
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 | ||
164 | |||
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 | |||
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 | |||
194 | |||
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 | ||
264 | |||
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 | ||
283 | |||
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 | ||
300 | |||
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 | ||
336 | |||
337 | |||
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 | ||
371 | |||
372 | |||
373 | |||
374 | |||
375 | |||
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_ | ||
385 | |||
386 | |||
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 | |||
393 | |||
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. | ||
411 | |||
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 | ||
415 | |||
416 | If possible, supply an IP address for the host address, | ||
417 | as this will bypass the DNS lookup. | ||
418 | """ | ||
419 | |||
420 | |||
421 | # | ||
422 | # Main | ||
423 | # | ||
424 | if __name__ == '__main__': | ||
425 | |||
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) | ||