check_nmap.py 11 KB

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