check_nmap.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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.20'
  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.20 2000-07-15 jaclu Updated params to correctly comply to plugin-standard
  28. # moved support classes to utils.py
  29. # 1.16 2000-07-14 jaclu made options and return codes more compatible with
  30. # the plugin developer-guidelines
  31. # 1.15 2000-07-14 jaclu added random string to temp-file name
  32. # 1.14 2000-07-14 jaclu added check for error from subproc
  33. # 1.10 2000-07-14 jaclu converted main part to class
  34. # 1.08 2000-07-13 jaclu better param parsing
  35. # 1.07 2000-07-13 jaclu changed nmap param to -P0
  36. # 1.06 2000-07-13 jaclu make sure tmp file is deleted on errors
  37. # 1.05 2000-07-12 jaclu in debug mode, show exit code
  38. # 1.03 2000-07-12 jaclu error handling on nmap output
  39. # 1.01 2000-07-12 jaclu added license
  40. # 1.00 2000-07-12 jaclu implemented timeout handling
  41. # 0.20 2000-07-10 jaclu Initial release
  42. import sys, os, string, whrandom
  43. import tempfile
  44. from getopt import getopt
  45. #
  46. # import generic Nagios-plugin stuff
  47. #
  48. import utils
  49. # Where temp files should be placed
  50. tempfile.tempdir='/usr/local/nagios/var'
  51. # Base name for tempfile
  52. tempfile.template='check_nmap_tmp.'
  53. # location and possibly params for nmap
  54. nmap_cmd='/usr/bin/nmap -P0'
  55. #
  56. # the class that does all the real work in this plugin...
  57. #
  58. #
  59. class CheckNmap:
  60. # Retcodes, so we are compatible with nagios
  61. #ERROR= -1
  62. UNKNOWN= -1
  63. OK= 0
  64. WARNING= 1
  65. CRITICAL= 2
  66. def __init__(self,cmd_line=[]):
  67. """Constructor.
  68. arguments:
  69. cmd_line: normaly sys.argv[1:] if called as standalone program
  70. """
  71. self.tmp_file=''
  72. self.host='' # host to check
  73. self.timeout=10
  74. self.debug=0 # 1= show debug info
  75. self.ports=[] # list of mandatory ports
  76. self.opt_ports=[] # list of optional ports
  77. self.ranges='' # port ranges for nmap
  78. self.exit_code=0 # numerical exit-code
  79. self.exit_msg='' # message to caller
  80. self.ParseCmdLine(cmd_line)
  81. def Run(self):
  82. """Actually run the process.
  83. This method should be called exactly once.
  84. """
  85. #
  86. # Only call check_host if cmd line was accepted earlier
  87. #
  88. if self.exit_code==0:
  89. self.CheckHost()
  90. self.CleanUp()
  91. return self.exit_code,self.exit_msg
  92. def Version(self):
  93. return 'check_nmap %s' % _version_
  94. #-----------------------------------------
  95. #
  96. # class internal stuff below...
  97. #
  98. #-----------------------------------------
  99. #
  100. # Param checks
  101. #
  102. def param2int_list(self,s):
  103. lst=string.split(string.replace(s,',',' '))
  104. try:
  105. for i in range(len(lst)):
  106. lst[i]=int(lst[i])
  107. except:
  108. lst=[]
  109. return lst
  110. def ParseCmdLine(self,cmd_line):
  111. try:
  112. opt_list=getopt(cmd_line,'vH:ho:p:r:t:V',['debug','host=','help',
  113. 'optional=','port=','range=','timeout','version'])
  114. for opt in opt_list[0]:
  115. if opt[0]=='-v' or opt[0]=='--debug':
  116. self.debug=1
  117. elif opt[0]=='-H' or opt[0]=='--host':
  118. self.host=opt[1]
  119. elif opt[0]=='-h' or opt[0]=='--help':
  120. doc_help()
  121. self.exit_code=1 # request termination
  122. break
  123. elif opt[0]=='-o' or opt[0]=='--optional':
  124. self.opt_ports=self.param2int_list(opt[1])
  125. elif opt[0]=='-p' or opt[0]=='--port':
  126. self.ports=self.param2int_list(opt[1])
  127. elif opt[0]=='-r' or opt[0]=='--range':
  128. r=string.replace(opt[1],':','-')
  129. self.ranges=r
  130. elif opt[0]=='-t' or opt[0]=='--timeout':
  131. self.timeout=opt[1]
  132. elif opt[0]=='-V' or opt[0]=='--version':
  133. print self.Version()
  134. self.exit_code=1 # request termination
  135. break
  136. else:
  137. self.host=''
  138. break
  139. except:
  140. # unknown param
  141. self.host=''
  142. if self.debug:
  143. print 'Params:'
  144. print '-------'
  145. print 'host = %s' % self.host
  146. print 'timeout = %s' % self.timeout
  147. print 'ports = %s' % self.ports
  148. print 'optional ports = %s' % self.opt_ports
  149. print 'ranges = %s' % self.ranges
  150. print
  151. #
  152. # a option that wishes us to terminate now has been given...
  153. #
  154. # This way, you can test params in debug mode and see what this
  155. # program recognised by suplying a version param at the end of
  156. # the cmd-line
  157. #
  158. if self.exit_code<>0:
  159. sys.exit(self.UNKNOWN)
  160. if self.host=='':
  161. doc_syntax()
  162. self.exit_code=self.UNKNOWN
  163. self.exit_msg='UNKNOWN: bad params, try running without any params for syntax'
  164. def CheckHost(self):
  165. 'Check one host using nmap.'
  166. #
  167. # Create a tmp file for storing nmap output
  168. #
  169. # The tempfile module from python 1.5.2 is stupid
  170. # two processes runing at aprox the same time gets
  171. # the same tempfile...
  172. # For this reason I use a random suffix for the tmp-file
  173. # Still not 100% safe, but reduces the risk significally
  174. # I also inserted checks at various places, so that
  175. # _if_ two processes in deed get the same tmp-file
  176. # the only result is a normal error message to nagios
  177. #
  178. r=whrandom.whrandom()
  179. self.tmp_file=tempfile.mktemp('.%s')%r.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. self.active_ports.append(int(p))
  248. except:
  249. # failure due to strange output...
  250. pass
  251. if self.debug:
  252. print 'Ports found by nmap: ',self.active_ports
  253. #
  254. # Filter out optional ports, we don't check status for them...
  255. #
  256. try:
  257. for p in self.opt_ports:
  258. self.active_ports.remove(p)
  259. if self.debug and len(self.opt_ports)>0:
  260. print 'optional ports removed:',self.active_ports
  261. except:
  262. # under extreame loads the remove(p) above failed for me
  263. # a few times, this exception hanlder handles
  264. # this bug-alike situation...
  265. pass
  266. opened=self.CheckOpen()
  267. closed=self.CheckClosed()
  268. if opened <>'':
  269. self.exit_code=self.CRITICAL
  270. self.exit_msg='PORTS CRITICAL - Open:%s Closed:%s'%(opened,closed)
  271. elif closed <>'':
  272. self.exit_code=self.WARNING
  273. self.exit_msg='PORTS WARNING - Closed:%s'%closed
  274. else:
  275. self.exit_code=self.OK
  276. self.exit_msg='PORTS ok - Only defined ports open'
  277. #
  278. # Compares requested ports on with actually open ports
  279. # returns all open that should be closed
  280. #
  281. def CheckOpen(self):
  282. opened=''
  283. for p in self.active_ports:
  284. if p not in self.ports:
  285. opened='%s %s' %(opened,p)
  286. return opened
  287. #
  288. # Compares requested ports with actually open ports
  289. # returns all ports that are should be open
  290. #
  291. def CheckClosed(self):
  292. closed=''
  293. for p in self.ports:
  294. if p not in self.active_ports:
  295. closed='%s %s' % (closed,p)
  296. return closed
  297. def CleanUp(self):
  298. #
  299. # If temp file exists, get rid of it
  300. #
  301. if self.tmp_file<>'' and os.path.isfile(self.tmp_file):
  302. try:
  303. os.remove(self.tmp_file)
  304. except:
  305. # temp-file colition, some other process already
  306. # removed the same file...
  307. pass
  308. #
  309. # Show numerical exits as string in debug mode
  310. #
  311. if self.debug:
  312. print 'Exitcode:',self.exit_code,
  313. if self.exit_code==self.UNKNOWN:
  314. print 'UNKNOWN'
  315. elif self.exit_code==self.OK:
  316. print 'OK'
  317. elif self.exit_code==self.WARNING:
  318. print 'WARNING'
  319. elif self.exit_code==self.CRITICAL:
  320. print 'CRITICAL'
  321. else:
  322. print 'undefined'
  323. #
  324. # Check if invalid exit code
  325. #
  326. if self.exit_code<-1 or self.exit_code>2:
  327. self.exit_msg=self.exit_msg+' - undefined exit code (%s)' % self.exit_code
  328. self.exit_code=self.UNKNOWN
  329. #
  330. # Help texts
  331. #
  332. def doc_head():
  333. print """
  334. check_nmap plugin for Nagios
  335. Copyright (c) 2000 Jacob Lundqvist (jaclu@galdrion.com)
  336. License: GPL
  337. Version: %s""" % _version_
  338. def doc_syntax():
  339. print """
  340. Usage: check_ports [-v|--debug] [-H|--host host] [-V|--version] [-h|--help]
  341. [-o|--optional port1,port2,port3 ...] [-r|--range range]
  342. [-p|--port port1,port2,port3 ...] [-t|--timeout timeout]"""
  343. def doc_help():
  344. 'Help is displayed if run without params.'
  345. doc_head()
  346. doc_syntax()
  347. print """
  348. Options:
  349. -h = help (this screen ;-)
  350. -v = debug mode, show some extra output
  351. -H host = host to check (name or IP#)
  352. -o ports = optional ports that can be open (one or more),
  353. no warning is given if optional port is closed
  354. -p ports = ports that should be open (one or more)
  355. -r range = port range to feed to nmap. Example: :1024,2049,3000:7000
  356. -t timeout = timeout in seconds, default 10
  357. -V = Version info
  358. This plugin attempts to verify open ports on the specified host.
  359. If all specified ports are open, OK is returned.
  360. If any of them are closed, WARNING is returned (except for optional ports)
  361. If other ports are open, CRITICAL is returned
  362. If possible, supply an IP address for the host address,
  363. as this will bypass the DNS lookup.
  364. """
  365. #
  366. # Main
  367. #
  368. if __name__ == '__main__':
  369. if len (sys.argv) < 2:
  370. #
  371. # No params given, show syntax and exit
  372. #
  373. doc_syntax()
  374. sys.exit(-1)
  375. nmap=CheckNmap(sys.argv[1:])
  376. exit_code,exit_msg=nmap.Run()
  377. #
  378. # Give Nagios a msg and a code
  379. #
  380. print exit_msg
  381. sys.exit(exit_code)