aionmap.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import asyncio
  4. import collections
  5. import os
  6. import re
  7. import shlex
  8. import sys
  9. from concurrent.futures import FIRST_COMPLETED
  10. from libnmap.parser import NmapParser
  11. regex_warning = re.compile('^Warning: .*', re.IGNORECASE)
  12. regex_nmap_version = re.compile('Nmap version [0-9]*\.[0-9]*[^ ]* \( https?://.* \)')
  13. regex_version = re.compile('[0-9]+')
  14. regex_subversion = re.compile('\.[0-9]+')
  15. class PortScannerBase(object):
  16. def __init__(self, nmap_search_path=(
  17. 'nmap', '/usr/bin/nmap', '/usr/local/bin/nmap', '/sw/bin/nmap', '/opt/local/bin/nmap')):
  18. self._nmap_path = '' # nmap path
  19. self._scan_result = {}
  20. self._nmap_version_number = 0 # nmap version number
  21. self._nmap_subversion_number = 0 # nmap subversion number
  22. self._nmap_search_path = nmap_search_path
  23. @asyncio.coroutine
  24. def _ensure_nmap_path_and_version(self):
  25. if self._nmap_path:
  26. return
  27. is_nmap_found = False
  28. for nmap_path in self._nmap_search_path:
  29. proc = None
  30. try:
  31. proc = yield from asyncio.create_subprocess_exec(nmap_path, '-V', stdout=asyncio.subprocess.PIPE)
  32. while True:
  33. line = yield from proc.stdout.readline()
  34. line = line.decode('utf8')
  35. if line and regex_nmap_version.match(line) is not None:
  36. is_nmap_found = True
  37. self._nmap_path = nmap_path
  38. # Search for version number
  39. rv = regex_version.search(line)
  40. rsv = regex_subversion.search(line)
  41. if rv is not None and rsv is not None:
  42. # extract version/subversion
  43. self._nmap_version_number = int(line[rv.start():rv.end()])
  44. self._nmap_subversion_number = int(
  45. line[rsv.start() + 1:rsv.end()]
  46. )
  47. break
  48. if proc.stdout.at_eof():
  49. break
  50. except:
  51. pass
  52. else:
  53. if is_nmap_found:
  54. break
  55. finally:
  56. if proc:
  57. try:
  58. proc.terminate()
  59. except ProcessLookupError:
  60. pass
  61. yield from proc.wait()
  62. if not is_nmap_found:
  63. raise NmapError('nmap program was not found in path')
  64. @asyncio.coroutine
  65. def nmap_version(self):
  66. yield from self._ensure_nmap_path_and_version()
  67. return (self._nmap_version_number, self._nmap_subversion_number)
  68. @asyncio.coroutine
  69. def listscan(self, hosts='127.0.0.1', dns_lookup=True, sudo=False, sudo_passwd=None):
  70. yield from self._ensure_nmap_path_and_version()
  71. nmap_args = self._get_scan_args(hosts, None, arguments='-sL' if dns_lookup else '-sL -n')
  72. return (yield from self._scan_proc(*nmap_args, sudo=sudo, sudo_passwd=sudo_passwd))
  73. def analyse_nmap_xml_scan(self, nmap_xml_output=None, nmap_err='', nmap_err_keep_trace='', nmap_warn_keep_trace=''):
  74. try:
  75. report = NmapParser.parse_fromstring(nmap_xml_output)
  76. report.__dict__['errors'] = nmap_err_keep_trace
  77. report.__dict__['warnings'] = nmap_warn_keep_trace
  78. return report
  79. except Exception:
  80. if len(nmap_err) > 0:
  81. raise NmapError(nmap_err)
  82. else:
  83. raise NmapError(nmap_xml_output)
  84. @asyncio.coroutine
  85. def _scan_proc(self, *nmap_args, sudo=False, sudo_passwd=None):
  86. proc = None
  87. try:
  88. if sudo:
  89. if not sudo_passwd:
  90. raise NmapError("sudo must with 'sudo_passwd' argument")
  91. proc = yield from asyncio.create_subprocess_exec('sudo', '-S', '-p', 'xxxxx', self._nmap_path,
  92. *nmap_args, stdin=asyncio.subprocess.PIPE,
  93. stdout=asyncio.subprocess.PIPE,
  94. stderr=asyncio.subprocess.PIPE)
  95. else:
  96. proc = yield from asyncio.create_subprocess_exec(self._nmap_path, *nmap_args,
  97. stdout=asyncio.subprocess.PIPE,
  98. stderr=asyncio.subprocess.PIPE)
  99. nmap_output, nmap_err = yield from proc.communicate(None if not sudo else (sudo_passwd.encode() + b"\n"))
  100. if nmap_err:
  101. if sudo and nmap_err.strip() == b'xxxxx':
  102. nmap_err = b''
  103. except:
  104. raise
  105. finally:
  106. if proc:
  107. try:
  108. proc.terminate()
  109. except ProcessLookupError:
  110. pass
  111. yield from proc.wait()
  112. if nmap_err:
  113. nmap_err = nmap_err.decode('utf8')
  114. if nmap_output:
  115. nmap_output = nmap_output.decode('utf8')
  116. nmap_err_keep_trace = []
  117. nmap_warn_keep_trace = []
  118. if len(nmap_err) > 0:
  119. for line in nmap_err.split(os.linesep):
  120. if len(line) > 0:
  121. rgw = regex_warning.search(line)
  122. if rgw is not None:
  123. # sys.stderr.write(line+os.linesep)
  124. nmap_warn_keep_trace.append(line + os.linesep)
  125. else:
  126. # raise NmapError(nmap_err)
  127. nmap_err_keep_trace.append(nmap_err)
  128. return self.analyse_nmap_xml_scan(
  129. nmap_xml_output=nmap_output,
  130. nmap_err=nmap_err,
  131. nmap_err_keep_trace=nmap_err_keep_trace,
  132. nmap_warn_keep_trace=nmap_warn_keep_trace
  133. )
  134. def _get_scan_args(self, hosts, ports, arguments):
  135. assert isinstance(hosts, (
  136. str, collections.Iterable)), 'Wrong type for [hosts], should be a string or Iterable [was {0}]'.format(
  137. type(hosts))
  138. assert isinstance(ports, (str, collections.Iterable, type(
  139. None))), 'Wrong type for [ports], should be a string or Iterable [was {0}]'.format(type(ports)) # noqa
  140. assert isinstance(arguments, str), 'Wrong type for [arguments], should be a string [was {0}]'.format(
  141. type(arguments)) # noqa
  142. if not isinstance(hosts, str):
  143. hosts = ' '.join(hosts)
  144. assert all(_ not in arguments for _ in ('-oX', '-oA')), 'Xml output can\'t be redirected from command line'
  145. if ports and not isinstance(ports, str):
  146. ports = ','.join(str(port) for port in ports)
  147. hosts_args = shlex.split(hosts)
  148. scan_args = shlex.split(arguments)
  149. return ['-oX', '-'] + hosts_args + ['-p', ports] * bool(ports) + scan_args
  150. class PortScanner(PortScannerBase):
  151. @asyncio.coroutine
  152. def scan(self, hosts='127.0.0.1', ports=None, arguments='-sV', sudo=False, sudo_passwd=None):
  153. yield from self._ensure_nmap_path_and_version()
  154. scan_result = yield from self._scan_proc(*(self._get_scan_args(hosts, ports, arguments)), sudo=sudo,
  155. sudo_passwd=sudo_passwd)
  156. self._scan_result = scan_result
  157. return scan_result
  158. def __getitem__(self, host):
  159. """
  160. returns a host detail
  161. """
  162. return self._scan_result['scan'][host]
  163. gt_py35 = (sys.version_info.major == 3 and sys.version_info.minor >= 5) or sys.version_info.major > 3
  164. if gt_py35:
  165. class PortScannerIterable(object):
  166. def __init__(self, scanner, hosts, args, batch_count=3, sudo=False, sudo_passwd=None):
  167. self._scanner = scanner
  168. self._hosts = hosts
  169. self._args = args
  170. self._futs = set()
  171. self._batch_count = batch_count
  172. self._stop_ip_gen = False
  173. self._done_fut_gen = None
  174. self._stopped = False
  175. self._started = False
  176. self.sudo = sudo
  177. self.sudo_passwd = sudo_passwd
  178. def _done_fu_generator(self, done_futs):
  179. yield from done_futs
  180. def _get_result(self):
  181. fu = self._done_fut_gen.send(None)
  182. exception = fu.exception()
  183. return exception if exception is not None else fu.result()
  184. async def __aiter__(self):
  185. return self
  186. def _ip_generator(self, ip_list):
  187. yield from ip_list
  188. def _fill_future(self):
  189. try:
  190. while len(self._futs) < self._batch_count:
  191. self._futs.add(asyncio.ensure_future(
  192. self._scanner._scan_proc(self._ip_gen.send(None), *self._args, sudo=self.sudo,
  193. sudo_passwd=self.sudo_passwd)))
  194. except StopIteration:
  195. self._stop_ip_gen = True
  196. async def __anext__(self):
  197. if not self._started:
  198. self._started = True
  199. list_scan = await self._scanner.listscan(self._hosts, False, sudo=self.sudo,
  200. sudo_passwd=self.sudo_passwd)
  201. if not list_scan:
  202. return
  203. ip_list = [i.address for i in list_scan.hosts]
  204. if not ip_list:
  205. raise StopAsyncIteration()
  206. self._ip_gen = self._ip_generator(ip_list)
  207. self._fill_future()
  208. elif self._done_fut_gen:
  209. try:
  210. return self._get_result()
  211. except:
  212. self._done_fut_gen = None
  213. if self._stopped:
  214. raise StopAsyncIteration()
  215. done, pending = await asyncio.wait(self._futs, return_when=FIRST_COMPLETED)
  216. self._done_fut_gen = self._done_fu_generator(done)
  217. if not pending and self._stop_ip_gen:
  218. self._stopped = True
  219. else:
  220. self._futs = pending
  221. if not self._stop_ip_gen:
  222. self._fill_future()
  223. if not self._futs and not pending:
  224. self._stopped = True
  225. return self._get_result()
  226. class PortScannerYield(PortScannerBase):
  227. def scan(self, hosts, ports, arguments, batch_count=3, sudo=False, sudo_passwd=None):
  228. args = self._get_scan_args('', ports, arguments)
  229. return PortScannerIterable(self, hosts, args, batch_count, sudo, sudo_passwd)
  230. class NmapError(Exception):
  231. """
  232. Exception error class for PortScanner class
  233. """
  234. def __init__(self, value):
  235. self.value = value
  236. def __str__(self):
  237. return repr(self.value)
  238. def __repr__(self):
  239. return 'NmapError exception {0}'.format(self.value)