check_nmap.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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, random
  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. self.tmp_file=tempfile.mktemp('.%s') % random.randint(0,100000)
  180. if self.debug:
  181. print 'Tmpfile is: %s'%self.tmp_file
  182. #
  183. # If a range is given, only run nmap on this range
  184. #
  185. if self.ranges<>'':
  186. global nmap_cmd # needed, to avoid error on next line
  187. # since we assigns to nmap_cmd :)
  188. nmap_cmd='%s -p %s' %(nmap_cmd,self.ranges)
  189. #
  190. # Prepare a task
  191. #
  192. t=utils.Task('%s %s' %(nmap_cmd,self.host))
  193. #
  194. # Configure a time-out handler
  195. #
  196. th=utils.TimeoutHandler(t.Kill, time_to_live=self.timeout,
  197. debug=self.debug)
  198. #
  199. # Fork of nmap cmd
  200. #
  201. t.Run(detach=0, stdout=self.tmp_file,stderr='/dev/null')
  202. #
  203. # Wait for completition, error or timeout
  204. #
  205. nmap_exit_code=t.Wait(idlefunc=th.Check, interval=1)
  206. #
  207. # Check for timeout
  208. #
  209. if th.WasTimeOut():
  210. self.exit_code=self.CRITICAL
  211. self.exit_msg='CRITICAL - Plugin timed out after %s seconds' % self.timeout
  212. return
  213. #
  214. # Check for exit status of subprocess
  215. # Must do this after check for timeout, since the subprocess
  216. # also returns error if aborted.
  217. #
  218. if nmap_exit_code <> 0:
  219. self.exit_code=self.UNKNOWN
  220. self.exit_msg='nmap program failed with code %s' % nmap_exit_code
  221. return
  222. #
  223. # Read output
  224. #
  225. try:
  226. f = open(self.tmp_file, 'r')
  227. output=f.readlines()
  228. f.close()
  229. except:
  230. self.exit_code=self.UNKNOWN
  231. self.exit_msg='Unable to get output from nmap'
  232. return
  233. #
  234. # Store open ports in list
  235. # scans for lines where first word contains '/'
  236. # and stores part before '/'
  237. #
  238. self.active_ports=[]
  239. try:
  240. for l in output:
  241. if len(l)<2:
  242. continue
  243. s=string.split(l)[0]
  244. if string.find(s,'/')<1:
  245. continue
  246. p=string.split(s,'/')[0]
  247. if string.find(l,'open')>1:
  248. self.active_ports.append(int(p))
  249. except:
  250. # failure due to strange output...
  251. pass
  252. if self.debug:
  253. print 'Ports found by nmap: ',self.active_ports
  254. #
  255. # Filter out optional ports, we don't check status for them...
  256. #
  257. try:
  258. for p in self.opt_ports:
  259. self.active_ports.remove(p)
  260. if self.debug and len(self.opt_ports)>0:
  261. print 'optional ports removed:',self.active_ports
  262. except:
  263. # under extreame loads the remove(p) above failed for me
  264. # a few times, this exception hanlder handles
  265. # this bug-alike situation...
  266. pass
  267. opened=self.CheckOpen()
  268. closed=self.CheckClosed()
  269. if opened <>'':
  270. self.exit_code=self.CRITICAL
  271. self.exit_msg='PORTS CRITICAL - Open:%s Closed:%s'%(opened,closed)
  272. elif closed <>'':
  273. self.exit_code=self.WARNING
  274. self.exit_msg='PORTS WARNING - Closed:%s'%closed
  275. else:
  276. self.exit_code=self.OK
  277. self.exit_msg='PORTS ok - Only defined ports open'
  278. #
  279. # Compares requested ports on with actually open ports
  280. # returns all open that should be closed
  281. #
  282. def CheckOpen(self):
  283. opened=''
  284. for p in self.active_ports:
  285. if p not in self.ports:
  286. opened='%s %s' %(opened,p)
  287. return opened
  288. #
  289. # Compares requested ports with actually open ports
  290. # returns all ports that are should be open
  291. #
  292. def CheckClosed(self):
  293. closed=''
  294. for p in self.ports:
  295. if p not in self.active_ports:
  296. closed='%s %s' % (closed,p)
  297. return closed
  298. def CleanUp(self):
  299. #
  300. # If temp file exists, get rid of it
  301. #
  302. if self.tmp_file<>'' and os.path.isfile(self.tmp_file):
  303. try:
  304. os.remove(self.tmp_file)
  305. except:
  306. # temp-file colition, some other process already
  307. # removed the same file...
  308. pass
  309. #
  310. # Show numerical exits as string in debug mode
  311. #
  312. if self.debug:
  313. print 'Exitcode:',self.exit_code,
  314. if self.exit_code==self.UNKNOWN:
  315. print 'UNKNOWN'
  316. elif self.exit_code==self.OK:
  317. print 'OK'
  318. elif self.exit_code==self.WARNING:
  319. print 'WARNING'
  320. elif self.exit_code==self.CRITICAL:
  321. print 'CRITICAL'
  322. else:
  323. print 'undefined'
  324. #
  325. # Check if invalid exit code
  326. #
  327. if self.exit_code<-1 or self.exit_code>2:
  328. self.exit_msg=self.exit_msg+' - undefined exit code (%s)' % self.exit_code
  329. self.exit_code=self.UNKNOWN
  330. #
  331. # Help texts
  332. #
  333. def doc_head():
  334. print """
  335. check_nmap plugin for Nagios
  336. Copyright (c) 2000 Jacob Lundqvist (jaclu@galdrion.com)
  337. License: GPL
  338. Version: %s""" % _version_
  339. def doc_syntax():
  340. print """
  341. Usage: check_nmap.py [-v|--debug] [-H|--host host] [-V|--version] [-h|--help]
  342. [-o|--optional port1,port2,port3 ...] [-r|--range range]
  343. [-p|--port port1,port2,port3 ...] [-t|--timeout timeout]"""
  344. def doc_help():
  345. 'Help is displayed if run without params.'
  346. doc_head()
  347. doc_syntax()
  348. print """
  349. Options:
  350. -h = help (this screen ;-)
  351. -v = debug mode, show some extra output
  352. -H host = host to check (name or IP#)
  353. -o ports = optional ports that can be open (one or more),
  354. no warning is given if optional port is closed
  355. -p ports = ports that should be open (one or more)
  356. -r range = port range to feed to nmap. Example: :1024,2049,3000:7000
  357. -t timeout = timeout in seconds, default 10
  358. -V = Version info
  359. This plugin attempts to verify open ports on the specified host.
  360. If all specified ports are open, OK is returned.
  361. If any of them are closed, WARNING is returned (except for optional ports)
  362. If other ports are open, CRITICAL is returned
  363. If possible, supply an IP address for the host address,
  364. as this will bypass the DNS lookup.
  365. """
  366. #
  367. # Main
  368. #
  369. if __name__ == '__main__':
  370. if len (sys.argv) < 2:
  371. #
  372. # No params given, show syntax and exit
  373. #
  374. doc_syntax()
  375. sys.exit(-1)
  376. nmap=CheckNmap(sys.argv[1:])
  377. exit_code,exit_msg=nmap.Run()
  378. #
  379. # Give Nagios a msg and a code
  380. #
  381. print exit_msg
  382. sys.exit(exit_code)