Advertisement
Guest User

Untitled

a guest
May 16th, 2016
208
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 35.82 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. '''
  4. #==================================================================================#
  5. # AUTHOR: Chris Gleason #
  6. # DATE: 1/12/2016 #
  7. # Version: 1.0 #
  8. # COMMENT: NetScan deamon to monitor state changes for network nodes #
  9. #==================================================================================#
  10. # Simple ICMP/TCP Netscanner to monitor state changes on the network #
  11. #==================================================================================#
  12.  
  13. ### DESCRIPTION/SYNOPSIS ###
  14.  
  15. This is a simple daemon that will allow you to scan a network range using ICMP, TCP or UDP and
  16. store the information in either memory or a file so that it can be referenced at a specific interval
  17. for state changes. It will allow you to log alerts or email them out to a specific email. It will
  18. also, eventually allow you to run it in the background, once I finish up the argparse variables...
  19.  
  20. ### FUTURE WORK ###
  21.  
  22. 1-14-2016 :
  23.  
  24. I am building this out as a basic ICMP scanner to store up and down states of specific
  25. nodes. I plan to add functions to do TCP and UDP SYN scanning as well as allow for out and in files
  26. to use to store and retrive state data. I also want to add functions for logging and emailing state
  27. alerts. But the first order of business is to get basic ICMP scanning and state alerting to the
  28. console.
  29.  
  30. 2-19-2016 :
  31.  
  32. - Handle the following Exceptions:
  33. * OSError: [Errno 65] No route to host
  34. - Fix --host parameter from changing state if node is up or down. Get initial scan to not count.
  35. - Fix email alerting so that it only sends out a single email per scan round.
  36. - Fix formatting of output table so tabs are lined up.
  37. - Add Validation for things like out and infile locations, IP Format, etc.
  38. - Add argument varaibles so argument values can be passed at runtime and script can be run in the
  39. backround.
  40.  
  41. 4-4-2016 :
  42.  
  43. Add scan finish statistics, wether it crashes or user kills it with Ctrl+c, output scann settings
  44. Number of times scanned and final version of DB with a close message.
  45.  
  46. 5-7-2016 :
  47.  
  48. Add a quiet option, so if they actually want to demonize it it will stay quiet and won't dump active
  49. data to the terminal. Then once killed or crashed it will print out the final state dict.
  50.  
  51. ### REQUIREMENTS ###
  52.  
  53. Requires Python 3 and OSX to run. If you read the script carefully you could redesign for python 2
  54. and for other platforms. The OSX specific subprocess calls and the subnet mask conversion are really
  55. the only platform dependent code.
  56.  
  57. ### NOTES ###
  58.  
  59. IPAddress module will do a lot of the heavy lifting with regards to calculating subnet nodes using a
  60. CIDR (https://docs.python.org/3/howto/ipaddress.html)
  61.  
  62. Seems like the socket generator has a hard time keeping up if the scanner runs to fast to often. Not
  63. sure if it's flood protection or the TCP stack coming unravelled. I haven't done any packet filtering to
  64. see what's happening at the packet level except to make sure the original scnaner is indeed sending
  65. raw ICMP, but I did find that if I set the time out too low and the scann frequency too low, that
  66. eventually the scanner will halt with a no route to host error. I found the sweet spot is:
  67.  
  68. timeout > .05 seconds
  69. frequency > 30 seconds
  70.  
  71. When running with these threshholds the scanner will run continuously with few issues. If you're
  72. scanning larger networks, it may make sense to lower the timeout. I find it works fine at .01 but will
  73. eventually fail if you scann to often, so set the frequency higher if you're scanning large subnets.
  74.  
  75. '''
  76.  
  77. __version__ = "$Revision: 1.0"
  78.  
  79. ###########
  80. # IMPORTS #
  81. ###########
  82.  
  83. import argparse
  84. import subprocess
  85. import os
  86. import ipaddress
  87. import sys
  88. import time
  89. import random
  90. import select
  91. import socket
  92. import csv
  93. import threading
  94. import smtplib
  95. from datetime import datetime
  96.  
  97. ###########################################
  98. # NON FUNCTION/CLASS SCRIPT RELATED STUFF #
  99. ###########################################
  100.  
  101. if os.geteuid() != 0:
  102. exit('''
  103.  
  104. This program creates and uses raw sockets which require rootn
  105. priviledges to run. Please run it as root in order to use it.
  106.  
  107. ''')
  108.  
  109. if sys.platform != 'darwin':
  110. print ("This script was designed to run on OSX. Currently that is the only platform it will work on.")
  111. exit(0)
  112.  
  113. parser = argparse.ArgumentParser(description='
  114. Network scanning daemon to check for node state changes via TCP/UDP/ICMP.
  115. Default (no arguments) will run in the foreground using ICMP and broadcast
  116. domain for discovery and will store state data in memory. Default (no arguments)
  117. uses true ICMP, so it's not usually routed. If you "ping scan" with NMAP that rides
  118. over TCP unless you specifically tell it to use the ICMP protocol, so if you are
  119. trying to scan a remote subnet, use the --tcp flag.')
  120.  
  121. parser.add_argument('-t', '--tcp' ,
  122. action='store_true' ,
  123. help='Use TCP SYN/ACK scanning for discovery')
  124. parser.add_argument('-q', '--quiet' ,
  125. action='store_true' ,
  126. help='Use to demonize netscanner for background processing - NOT IMPLEMENTED YET')
  127. parser.add_argument('-i', '--infile' ,
  128. action='store_true' ,
  129. help='Use an existing CSV file instead of scanning the network for initial discovery')
  130. parser.add_argument('-o', '--outfile' ,
  131. action='store_true' ,
  132. help='Export stat data to a CSV file')
  133. parser.add_argument('-c', '--cidr' ,
  134. action='store_true' ,
  135. help='Use a CIDR block to generate scan range instead of using the broadcast domain')
  136. parser.add_argument('-H', '--host' ,
  137. action='store_true' ,
  138. help='Monitor the state of a single host')
  139. parser.add_argument('-e', '--email' ,
  140. action='store_true' ,
  141. help='Use a gmail account to send state change alerts to a desired email as well as the console')
  142. parser.add_argument('-l', '--logging' ,
  143. action='store_true' ,
  144. help='Log state changes to system logs as well as the console')
  145.  
  146. args = parser.parse_args()
  147. ip = ""
  148. nm = ""
  149. dd_nm = ""
  150. tout = .1
  151. iface = ""
  152. state_dict = {}
  153. freq = ""
  154. count = 0
  155. rtt = ""
  156. ofile = ""
  157. alert_total = ""
  158. totalruns = 0
  159. t1 = datetime.now()
  160.  
  161. #############
  162. # FUNCTIONS #
  163. #############
  164.  
  165.  
  166. def output_title(title):
  167.  
  168. '''
  169. Function to auto-generate output headers and titles
  170.  
  171. output=string
  172. '''
  173.  
  174. titlelen = len(title)
  175. print('=' * titlelen)
  176. print(title)
  177. print('=' * titlelen)
  178.  
  179. def get_tout(a):
  180.  
  181. '''
  182. Function to solicit timeout from the user
  183. '''
  184.  
  185. global tout
  186.  
  187. print()
  188. tout = input('What timeout would you like to use (in seconds and you can use decimal numbers): ')
  189. print()
  190.  
  191. return tout
  192.  
  193. def get_net_size(netmask):
  194.  
  195. '''
  196. Function that helps convert netmask and IP into CIDR block.
  197. This code was borrowed. I can tell that it turns the inegerized octet of the Hex NetMask
  198. into a binary number that is then pumped into zfill and stripped of zeros. How it
  199. actually converts this into a CIDR block I'm not entirely sure yet. I'll figure it out
  200. later.
  201. '''
  202.  
  203. binary_str = ''
  204. for octet in netmask:
  205. binary_str += bin(int(octet))[2:].zfill(8)
  206. return str(len(binary_str.rstrip('0')))
  207.  
  208. def get_net_info():
  209.  
  210. '''
  211. Function that pulls net info from the host converts it into subnet info, calculates hosts
  212. list and dumps it into an array
  213.  
  214. output=strings and a dictionary
  215. '''
  216.  
  217. global ip
  218. global cidr
  219. global dd_nm
  220. global iface
  221. global tout
  222.  
  223. iface = input('What interface would you like to use: ')
  224. #get_tout(tout)
  225.  
  226. # Get IP from subprocess
  227.  
  228. ipcmd = "ifconfig %s | grep netmask | awk {'print $2'}" % (iface)
  229. ip = subprocess.Popen(ipcmd , shell=True, stdout=subprocess.PIPE)
  230. ip = ip.stdout.read()
  231. ip = str(ip).strip('b').strip(''').strip('\n')
  232.  
  233. # Get Netmask from subprocess
  234.  
  235. nmcmd = "ifconfig %s | grep netmask | awk {'print $4'}" % (iface)
  236. nm = subprocess.Popen(nmcmd , shell=True, stdout=subprocess.PIPE)
  237. nm = nm.stdout.read()
  238. nm = str(nm).strip('b').strip(''').strip('\n')
  239.  
  240. # Convert hexmask to dotted decimal
  241.  
  242. i = nm
  243. prefix = i[0:2]
  244. first = i[2:4]
  245. second = i[4:6]
  246. third = i[6:8]
  247. forth = i[8:10]
  248.  
  249. oct1 = "0x{}".format(first)
  250. oct2 = "0x{}".format(second)
  251. oct3 = "0x{}".format(third)
  252. oct4 = "0x{}".format(forth)
  253.  
  254. oct1 = int(oct1, 0)
  255. oct2 = int(oct2, 0)
  256. oct3 = int(oct3, 0)
  257. oct4 = int(oct4, 0)
  258.  
  259. dd_nm = ("" + str(oct1) + "." + str(oct2) + "." + str(oct3) + "." + str(oct4))
  260. dd_nm = str(dd_nm)
  261.  
  262. # Convert IP and dotted decimal netmask to a CIDR block
  263.  
  264. splitip = ip.split('.')
  265. splitnm = dd_nm.split('.')
  266. net_start = [str(int(splitip[x]) & int(splitnm[x]))
  267. for x in range(0,4)]
  268. cidr = str('.'.join(net_start) + '/' + get_net_size(splitnm))
  269.  
  270. ### RETURNS ###
  271.  
  272. return cidr
  273. return dd_nm
  274. return ip
  275. return iface
  276. return tout
  277.  
  278. def print_net_info(a, b, c):
  279.  
  280. '''
  281. Test function to see what is being returned after each stage
  282. '''
  283.  
  284. print ()
  285.  
  286. title="NETWORK INFORMATION"
  287. output_title(title)
  288.  
  289. global cidr
  290. global dd_nm
  291. global ip
  292.  
  293. print ()
  294. print ("IP is " + b)
  295. print ("Netmask is " + c)
  296. print ("CIDR is " + a)
  297.  
  298. def chk(data):
  299.  
  300. '''
  301. Function that validates data being sent to ping function
  302. '''
  303.  
  304. x = sum(a + b * 256 for a, b in zip(data[::2], data[1::2] + b'x00')) & 0xFFFFFFFF
  305. x = (x >> 16) + (x & 0xFFFF)
  306. x = (x >> 16) + (x & 0xFFFF)
  307. return (~x & 0xFFFF).to_bytes(2, 'little')
  308.  
  309. def ping(addr, timeout=tout):
  310.  
  311. '''
  312. This Function creates a raw socket using ICMP, then connects to an address
  313. using that socket, recording the time it takes to return. You can specify
  314. timeout in the functions arguments. Currently uses user input.
  315. '''
  316.  
  317. with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) as conn:
  318. payload = random.randrange(0, 65536).to_bytes(2, 'big') + b'x01x00'
  319. packet = b'x08x00' + b'x00x00' + payload
  320. packet = b'x08x00' + chk(packet) + payload
  321. conn.connect((addr, 80))
  322. conn.sendall(packet)
  323.  
  324. start = time.time()
  325.  
  326. while select.select([conn], [], [], max(0, start + timeout - time.time()))[0]:
  327. packet = conn.recv(1024)[20:]
  328. unchecked = packet[:2] + b'' + packet[4:]
  329.  
  330. if packet == b'' + chk(unchecked) + payload:
  331. return time.time() - start
  332.  
  333.  
  334. def tcp_scan(addr, port, timeout=tout):
  335.  
  336. '''
  337. Function for scanning with TCP
  338. '''
  339. global result
  340.  
  341. s= socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  342. s.settimeout(timeout)
  343. result = s.connect_ex((addr, port))
  344. #print ('Scanning - ' + addr)
  345. s.settimeout(None)
  346. s.close()
  347. return result
  348.  
  349.  
  350. def initial_net_scan(a):
  351.  
  352. '''
  353. Function takes cidr variable from get_net_info, creates a list of IP's
  354. then scans them all using the ping function
  355. '''
  356.  
  357. global totalruns
  358. global state_dict
  359.  
  360. net4 = ipaddress.ip_network(a)
  361. print ()
  362. print ("Calculating network host list and scanning.")
  363. print ()
  364. print ("Please be patient, this may take some time:")
  365. print ()
  366. if not (args.tcp): # and not (args.udp):
  367. for x in net4.hosts():
  368. state_dict.update({x : [ping(str(x), float(tout)), 0]})
  369. if args.tcp:
  370. for x in net4.hosts():
  371. #state_dict.update({x : [0, 0]})
  372. #for a,b in state_dict.items():
  373. start = time.time()
  374. tcp_scan(str(x), int(port), float(tout))
  375. if result != 0:
  376. rtt = None
  377. else:
  378. rtt = time.time() - start
  379. state_dict.update({x : [rtt, 0]})
  380.  
  381.  
  382. # start = time.time()
  383. # tcp_scan(str(host), int(port))
  384. # if result != 0:
  385. # rtt2 = None
  386. # else:
  387. # rtt2 = time.time() - start
  388.  
  389.  
  390. totalruns += 1
  391.  
  392. return totalruns
  393. return state_dict
  394.  
  395. def redundant_net_scan(a, host, port):
  396.  
  397. '''
  398. Function takes the state_dict generated from the initial_net_scan function
  399. then scans the IP's again an calculates if the state has changed
  400. '''
  401.  
  402. global totalruns
  403. global count
  404. global state_dict
  405. global alert_total
  406.  
  407. print ("Rescanning, this may take some time:")
  408. print ()
  409. alert_total = ""
  410. for x,y in a.items():
  411. print_ip = x
  412. rtt1 = y[0]
  413. if args.tcp:
  414. start = time.time()
  415. tcp_scan(str(x), int(port), float(tout))
  416. if result != 0:
  417. rtt2 = None
  418. else:
  419. rtt2 = time.time() - start
  420. else:
  421. rtt2 = ping(str(x), float(tout))
  422. count = y[1]
  423. if type(rtt1).__name__ == "float" and type(rtt2).__name__ == "NoneType"
  424. or type(rtt1).__name__ == "NoneType" and type(rtt2).__name__ == "float":
  425. alert = "State changed for " + str(print_ip) + ". It went from " + str(rtt1) + " to " + str(rtt2) + ".n"
  426. alert_total+=alert
  427. count = y[1] + 1
  428. state_dict.update({x : [rtt2, count]})
  429. count = 0
  430. else:
  431. state_dict.update({x : [rtt2, count]})
  432.  
  433. if args.logging:
  434. #print(alert_total)
  435. log_alert(alert_total)
  436.  
  437. if args.email:
  438. email_alert(toaddrs, username, password, alert_total)
  439.  
  440. print(alert_total)
  441.  
  442. totalruns += 1
  443.  
  444. return count
  445. return state_dict
  446. return alert_total
  447. return totalruns
  448.  
  449. def print_dict(sd):
  450.  
  451. '''
  452. Prints out state dictionary in formatted output
  453. '''
  454.  
  455. global state_dict
  456.  
  457. for x,y in sd.items():
  458. print_ip = x
  459. print_rtt = y[0]
  460. print_count = y[1]
  461. print ("IP: " + str(print_ip) + "ttRTT: " + str(print_rtt) + "tttChange Count: " + str(print_count))
  462.  
  463. def csv_writer(ofile):
  464.  
  465. '''
  466. Function to take State Dictionary and output to to CSV file
  467. '''
  468.  
  469. writer = csv.writer(open(ofile, 'w'))
  470. for x,y in state_dict.items():
  471. writer.writerow([x, y[0], y[1]])
  472.  
  473. def email_alert(toaddrs, username, password, alerti_total):
  474.  
  475. '''
  476. Function to send state change list to designated email
  477. '''
  478.  
  479. fromaddr = 'netscanalert@chrisgleason.com'
  480. msg = alert_total
  481. server = smtplib.SMTP('smtp.gmail.com:587')
  482. server.starttls()
  483. server.login(username,password)
  484. server.sendmail(fromaddr, toaddrs, msg)
  485. server.quit()
  486.  
  487. def log_alert(alert):
  488.  
  489. '''
  490. Function to take state change list and log in syslog
  491. '''
  492.  
  493. subprocess.Popen("logger " + alert, shell=True, stdout=subprocess.PIPE)
  494.  
  495. ############
  496. # MAIN RUN #
  497. ############
  498.  
  499.  
  500. if __name__ == "__main__":
  501.  
  502. '''
  503. Main Code run
  504. '''
  505.  
  506.  
  507. try:
  508.  
  509. title='Netscanner - Network state discovery and change alerter daemon'
  510. output_title(title)
  511. print()
  512. print('Hit Ctrl+C to kill the deamon if it's running in the foreground')
  513. print()
  514.  
  515. print()
  516. freq = input('What frequency would you like the scanner to run (in seconds): ')
  517. print()
  518.  
  519. get_tout(tout)
  520.  
  521. if args.tcp: # or args.udp:
  522. port = input('What port would you like to use to scan against? : ')
  523. print ()
  524. else:
  525. port = 0
  526.  
  527. if args.cidr or args.infile or args.host:
  528. pass
  529. else:
  530. get_net_info()
  531.  
  532. if args.cidr:
  533. cidr = input('What CIDR block would you like to use (use X.X.X.X/XXX format) : ')
  534. #get_tout(tout)
  535. print ()
  536. print ('You chose CIDR block: ' + cidr)
  537.  
  538. if args.infile:
  539. ifile = input('Please specify the explicit path to the file you want to import: ')
  540. reader = csv.reader(open(ifile, 'r'))
  541. state_dict = {}
  542. for row in reader:
  543. ip, rtt, count = row
  544. if rtt == '':
  545. rtt = 'None'
  546. state_dict[ip] = [rtt, count]
  547.  
  548. if args.outfile:
  549. ofile = input('Please specify the explicit path to the file you want to export the data to: ')
  550.  
  551. if args.host:
  552. global host
  553. host = input('What host would you like to scan (Use an IP in dotted decimal format X.X.X.X): ')
  554. if not (args.tcp): # and not (args.udp):
  555. rtt = ping(str(host), float(tout))
  556. state_dict.update({host : [rtt, count]})
  557. if args.tcp:
  558. start = time.time()
  559. tcp_scan(str(host), int(port))
  560. if result != 0:
  561. rtt = None
  562. else:
  563. rtt = time.time() - start
  564. state_dict.update({host : [rtt, count]})
  565. cidr = host + "/32"
  566. else:
  567. host = '0.0.0.0'
  568.  
  569. if args.email:
  570. toaddrs = input('What email are you sending alerts to: ')
  571. username = input('What is your gmail username: ')
  572. password = input('What is your gmail password (you may need an application specific password): ')
  573.  
  574. if args.cidr or args.infile:
  575. pass
  576. else:
  577. print_net_info(cidr, ip, dd_nm)
  578.  
  579. print ()
  580. input('Press Enter to start the scan')
  581. if not args.infile:
  582. os.system('clear')
  583. initial_net_scan(cidr)
  584. print_dict(state_dict)
  585. if args.outfile:
  586. csv_writer(ofile)
  587. time.sleep(int(freq))
  588.  
  589. while True:
  590. os.system('clear')
  591. redundant_net_scan(state_dict, host, port)
  592. print_dict(state_dict)
  593. if args.outfile:
  594. csv_writer(ofile)
  595. time.sleep(int(freq))
  596.  
  597. except KeyboardInterrupt:
  598. t2 = datetime.now()
  599. timetotal = t2 - t1
  600. print ()
  601. print ("===================================================================================")
  602. print ()
  603. print_dict(state_dict)
  604. print ()
  605. print ("===================================================================================")
  606. print ()
  607. print ("You pressed Ctrl+C")
  608. print ()
  609. print ("Your scan ran through " + str(totalruns) + " cycles, every " + str(freq) + " seconds.")
  610. print ()
  611. print ("The scan ran for a total of " + str(timetotal))
  612. print ()
  613. print ("The final data set is above:")
  614. sys.exit()
  615.  
  616. except OSError as e:
  617. t2 = datetime.now()
  618. timetotal = t2 - t1
  619. print ()
  620. print ('Script crashed')
  621. print ('Dumping state dict')
  622. print ()
  623. print ('====================================================================================')
  624. print ()
  625. print_dict(state_dict)
  626. print ()
  627. print ('====================================================================================')
  628. print ()
  629. print ('There was an OS Error exception, most likely a no route to host. for now, I'm just')
  630. print ('dumping the last version of the state dictionary and exiting.')
  631. print ()
  632. print ("Your scan ran through " + str(totalruns) + " cycles, every " + str(freq) + " seconds.")
  633. print ()
  634. print ("The scan ran for a total of " + str(timetotal))
  635. print ()
  636. print ('If you're seeing this error a lot, try changing the frequency to at least 30 seconds, and')
  637. print ('set the timeout to at least .1 for a trial. If it stops, you can tune it down. If it keeps')
  638. print ('failing, then you should increase both thresholds until it stops')
  639.  
  640. import argparse
  641. import csv
  642. import ipaddress
  643. import os
  644. import random
  645. import select
  646. import smtplib
  647. import socket
  648. import subprocess
  649. import sys
  650. import time
  651. from datetime import datetime
  652.  
  653. parser.add_argument('-t', '--tcp' ,
  654. action='store_true' ,
  655. help='Use TCP SYN/ACK scanning for discovery')
  656.  
  657. parser.add_argument('-t', '--tcp',
  658. action='store_true',
  659. help='Use TCP SYN/ACK scanning for discovery')
  660.  
  661. ###########################################
  662. # NON FUNCTION/CLASS SCRIPT RELATED STUFF #
  663. ###########################################
  664.  
  665. # Non function / class related stuff
  666.  
  667. def output_title(title):
  668.  
  669. '''
  670. Function to auto-generate output headers and titles
  671.  
  672. output=string
  673. '''
  674.  
  675. def output_title(title):
  676. # Function to auto-generate output headers and titles(output=string)
  677. '''
  678.  
  679. def output_title(title):
  680. ....
  681.  
  682.  
  683. def get_tout(a):
  684. ....
  685.  
  686. payload = random.randrange(0, 65536).to_bytes(2, 'big') + b'x01x00'
  687. packet = b'x08x00' + b'x00x00' + payload
  688. packet = b'x08x00' + chk(packet) + payload
  689.  
  690. payload = random.randrange(0, 65536).to_bytes(2, 'big') + b'x01x00'
  691. packet = b'x08x00' + b'x00x00' + payload
  692. packet = b'x08x00' + chk(packet) + payload
  693.  
  694. if not (args.tcp):
  695. ....
  696.  
  697. if not args.tcp:
  698. ....
  699.  
  700. import argparse
  701. import csv
  702. import ipaddress
  703. import os
  704. import random
  705. import select
  706. import smtplib
  707. import socket
  708. import subprocess
  709. import sys
  710. import time
  711. from datetime import datetime
  712.  
  713. # Non function / class related stuff
  714.  
  715. if os.geteuid() != 0:
  716. exit('''
  717.  
  718. This program creates and uses raw sockets which require rootn
  719. priviledges to run. Please run it as root in order to use it.
  720.  
  721. ''')
  722.  
  723. if sys.platform != 'darwin':
  724. print("This script was designed to run on OSX. Currently that is the only platform it will work on.")
  725. exit(0)
  726.  
  727. parser = argparse.ArgumentParser(description='
  728. Network scanning daemon to check for node state changes via TCP/UDP/ICMP.
  729. Default (no arguments) will run in the foreground using ICMP and broadcast
  730. domain for discovery and will store state data in memory. Default (no arguments)
  731. uses true ICMP, so it's not usually routed. If you "ping scan" with NMAP that rides
  732. over TCP unless you specifically tell it to use the ICMP protocol, so if you are
  733. trying to scan a remote subnet, use the --tcp flag.')
  734.  
  735. parser.add_argument('-t', '--tcp',
  736. action='store_true',
  737. help='Use TCP SYN/ACK scanning for discovery')
  738. parser.add_argument('-q', '--quiet',
  739. action='store_true',
  740. help='Use to demonize netscanner for background processing - NOT IMPLEMENTED YET')
  741. parser.add_argument('-i', '--infile',
  742. action='store_true',
  743. help='Use an existing CSV file instead of scanning the network for initial discovery')
  744. parser.add_argument('-o', '--outfile',
  745. action='store_true',
  746. help='Export stat data to a CSV file')
  747. parser.add_argument('-c', '--cidr',
  748. action='store_true',
  749. help='Use a CIDR block to generate scan range instead of using the broadcast domain')
  750. parser.add_argument('-H', '--host',
  751. action='store_true',
  752. help='Monitor the state of a single host')
  753. parser.add_argument('-e', '--email',
  754. action='store_true',
  755. help='Use a gmail account to send state change alerts to a desired email as well as the console')
  756. parser.add_argument('-l', '--logging',
  757. action='store_true',
  758. help='Log state changes to system logs as well as the console')
  759.  
  760. args = parser.parse_args()
  761. ip = ""
  762. nm = ""
  763. dd_nm = ""
  764. tout = .1
  765. iface = ""
  766. state_dict = {}
  767. freq = ""
  768. count = 0
  769. rtt = ""
  770. ofile = ""
  771. alert_total = ""
  772. totalruns = 0
  773. t1 = datetime.now()
  774.  
  775.  
  776. # Functions
  777.  
  778.  
  779. def output_title(title):
  780. # Function to auto-generate output headers and titles(output=string)
  781.  
  782. titlelen = len(title)
  783. print('=' * titlelen)
  784. print(title)
  785. print('=' * titlelen)
  786.  
  787.  
  788. def get_tout(a):
  789. # Function to solicit timeout from the user
  790.  
  791. global tout
  792.  
  793. print()
  794. tout = input('What timeout would you like to use (in seconds and you can use decimal numbers): ')
  795. print()
  796.  
  797. return tout
  798.  
  799.  
  800. def get_net_size(netmask):
  801. '''
  802. Function that helps convert netmask and IP into CIDR block.
  803. This code was borrowed. I can tell that it turns the inegerized octet of the Hex NetMask
  804. into a binary number that is then pumped into zfill and stripped of zeros. How it
  805. actually converts this into a CIDR block I'm not entirely sure yet. I'll figure it out
  806. later.
  807. '''
  808.  
  809. binary_str = ''
  810. for octet in netmask:
  811. binary_str += bin(int(octet))[2:].zfill(8)
  812. return str(len(binary_str.rstrip('0')))
  813.  
  814.  
  815. def get_net_info():
  816. '''
  817. Function that pulls net info from the host converts it into subnet info, calculates hosts
  818. list and dumps it into an array
  819.  
  820. output=strings and a dictionary
  821. '''
  822.  
  823. global ip
  824. global cidr
  825. global dd_nm
  826. global iface
  827. global tout
  828.  
  829. iface = input('What interface would you like to use: ')
  830. # get_tout(tout)
  831.  
  832. # Get IP from subprocess
  833.  
  834. ipcmd = "ifconfig %s | grep netmask | awk {'print $2'}" % (iface)
  835. ip = subprocess.Popen(ipcmd, shell=True, stdout=subprocess.PIPE)
  836. ip = ip.stdout.read()
  837. ip = str(ip).strip('b').strip(''').strip('\n')
  838.  
  839. # Get Netmask from subprocess
  840.  
  841. nmcmd = "ifconfig %s | grep netmask | awk {'print $4'}" % (iface)
  842. nm = subprocess.Popen(nmcmd, shell=True, stdout=subprocess.PIPE)
  843. nm = nm.stdout.read()
  844. nm = str(nm).strip('b').strip(''').strip('\n')
  845.  
  846. # Convert hexmask to dotted decimal
  847.  
  848. i = nm
  849. prefix = i[0:2]
  850. first = i[2:4]
  851. second = i[4:6]
  852. third = i[6:8]
  853. forth = i[8:10]
  854.  
  855. oct1 = "0x{}".format(first)
  856. oct2 = "0x{}".format(second)
  857. oct3 = "0x{}".format(third)
  858. oct4 = "0x{}".format(forth)
  859.  
  860. oct1 = int(oct1, 0)
  861. oct2 = int(oct2, 0)
  862. oct3 = int(oct3, 0)
  863. oct4 = int(oct4, 0)
  864.  
  865. dd_nm = ("" + str(oct1) + "." + str(oct2) + "." + str(oct3) + "." + str(oct4))
  866. dd_nm = str(dd_nm)
  867.  
  868. # Convert IP and dotted decimal netmask to a CIDR block
  869.  
  870. splitip = ip.split('.')
  871. splitnm = dd_nm.split('.')
  872. net_start = [str(int(splitip[x]) & int(splitnm[x]))
  873. for x in range(0, 4)]
  874. cidr = str('.'.join(net_start) + '/' + get_net_size(splitnm))
  875.  
  876. return cidr
  877. return dd_nm
  878. return ip
  879. return iface
  880. return tout
  881.  
  882.  
  883. def print_net_info(a, b, c):
  884. # Test function to see what is being returned after each stage
  885.  
  886. print()
  887. title = "NETWORK INFORMATION"
  888. output_title(title)
  889.  
  890. global cidr
  891. global dd_nm
  892. global ip
  893.  
  894. print()
  895. print("IP is " + b)
  896. print("Netmask is " + c)
  897. print("CIDR is " + a)
  898.  
  899.  
  900. def chk(data):
  901. # Function that validates data being sent to ping function
  902.  
  903. x = sum(a + b * 256 for a, b in zip(data[::2], data[1::2] + b'x00')) & 0xFFFFFFFF
  904. x = (x >> 16) + (x & 0xFFFF)
  905. x = (x >> 16) + (x & 0xFFFF)
  906. return (~x & 0xFFFF).to_bytes(2, 'little')
  907.  
  908.  
  909. def ping(addr, timeout=tout):
  910. '''
  911. This Function creates a raw socket using ICMP, then connects to an address
  912. using that socket, recording the time it takes to return. You can specify
  913. timeout in the functions arguments. Currently uses user input.
  914. '''
  915.  
  916. with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) as conn:
  917. payload = random.randrange(0, 65536).to_bytes(2, 'big') + b'x01x00'
  918. packet = b'x08x00' + b'x00x00' + payload
  919. packet = b'x08x00' + chk(packet) + payload
  920. conn.connect((addr, 80))
  921. conn.sendall(packet)
  922.  
  923. start = time.time()
  924.  
  925. while select.select([conn], [], [], max(0, start + timeout - time.time()))[0]:
  926. packet = conn.recv(1024)[20:]
  927. unchecked = packet[:2] + b'' + packet[4:]
  928.  
  929. if packet == b'' + chk(unchecked) + payload:
  930. return time.time() - start
  931.  
  932.  
  933. def tcp_scan(addr, port, timeout=tout):
  934. # Function for scanning with TCP
  935.  
  936. global result
  937.  
  938. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  939. s.settimeout(timeout)
  940. result = s.connect_ex((addr, port))
  941. # print ('Scanning - ' + addr)
  942. s.settimeout(None)
  943. s.close()
  944. return result
  945.  
  946.  
  947. def initial_net_scan(a):
  948. '''
  949. Function takes cidr variable from get_net_info, creates a list of IP's
  950. then scans them all using the ping function
  951. '''
  952.  
  953. global totalruns
  954. global state_dict
  955.  
  956. net4 = ipaddress.ip_network(a)
  957. print()
  958. print("Calculating network host list and scanning.")
  959. print()
  960. print("Please be patient, this may take some time:")
  961. print()
  962. if not (args.tcp): # and not (args.udp):
  963. for x in net4.hosts():
  964. state_dict.update({x: [ping(str(x), float(tout)), 0]})
  965. if args.tcp:
  966. for x in net4.hosts():
  967. # state_dict.update({x : [0, 0]})
  968. # for a,b in state_dict.items():
  969. start = time.time()
  970. tcp_scan(str(x), int(port), float(tout))
  971. if result != 0:
  972. rtt = None
  973. else:
  974. rtt = time.time() - start
  975. state_dict.update({x: [rtt, 0]})
  976.  
  977. totalruns += 1
  978.  
  979. return totalruns
  980. return state_dict
  981.  
  982.  
  983. def redundant_net_scan(a, host, port):
  984. '''
  985. Function takes the state_dict generated from the initial_net_scan function
  986. then scans the IP's again an calculates if the state has changed
  987. '''
  988.  
  989. global totalruns
  990. global count
  991. global state_dict
  992. global alert_total
  993.  
  994. print("Rescanning, this may take some time:")
  995. print()
  996. alert_total = ""
  997. for x, y in a.items():
  998. print_ip = x
  999. rtt1 = y[0]
  1000. if args.tcp:
  1001. start = time.time()
  1002. tcp_scan(str(x), int(port), float(tout))
  1003. if result != 0:
  1004. rtt2 = None
  1005. else:
  1006. rtt2 = time.time() - start
  1007. else:
  1008. rtt2 = ping(str(x), float(tout))
  1009. count = y[1]
  1010. if type(rtt1).__name__ == "float" and type(rtt2).__name__ == "NoneType"
  1011. or type(rtt1).__name__ == "NoneType" and type(rtt2).__name__ == "float":
  1012. alert = "State changed for " + str(print_ip) + ". It went from " + str(rtt1) + " to " + str(rtt2) + ".n"
  1013. alert_total += alert
  1014. count = y[1] + 1
  1015. state_dict.update({x: [rtt2, count]})
  1016. count = 0
  1017. else:
  1018. state_dict.update({x: [rtt2, count]})
  1019.  
  1020. if args.logging:
  1021. # print(alert_total)
  1022. log_alert(alert_total)
  1023.  
  1024. if args.email:
  1025. email_alert(toaddrs, username, password, alert_total)
  1026.  
  1027. print(alert_total)
  1028.  
  1029. totalruns += 1
  1030.  
  1031. return count
  1032. return state_dict
  1033. return alert_total
  1034. return totalruns
  1035.  
  1036.  
  1037. def print_dict(sd):
  1038. # Prints out state dictionary in formatted output
  1039.  
  1040. global state_dict
  1041.  
  1042. for x, y in sd.items():
  1043. print_ip = x
  1044. print_rtt = y[0]
  1045. print_count = y[1]
  1046. print("IP: " + str(print_ip) + "ttRTT: " + str(print_rtt) + "tttChange Count: " + str(print_count))
  1047.  
  1048.  
  1049. def csv_writer(ofile):
  1050. # Function to take State Dictionary and output to to CSV file
  1051.  
  1052. writer = csv.writer(open(ofile, 'w'))
  1053. for x, y in state_dict.items():
  1054. writer.writerow([x, y[0], y[1]])
  1055.  
  1056.  
  1057. def email_alert(toaddrs, username, password, alerti_total):
  1058. # Function to send state change list to designated email
  1059.  
  1060. fromaddr = 'netscanalert@chrisgleason.com'
  1061. msg = alert_total
  1062. server = smtplib.SMTP('smtp.gmail.com:587')
  1063. server.starttls()
  1064. server.login(username, password)
  1065. server.sendmail(fromaddr, toaddrs, msg)
  1066. server.quit()
  1067.  
  1068.  
  1069. def log_alert(alert):
  1070. # Function to take state change list and log in syslog
  1071.  
  1072. subprocess.Popen("logger " + alert, shell=True, stdout=subprocess.PIPE)
  1073.  
  1074.  
  1075. if __name__ == "__main__":
  1076.  
  1077. try:
  1078.  
  1079. title = 'Netscanner - Network state discovery and change alerter daemon'
  1080. output_title(title)
  1081. print()
  1082. print('Hit Ctrl+C to kill the deamon if it's running in the foreground')
  1083. print()
  1084.  
  1085. print()
  1086. freq = input('What frequency would you like the scanner to run (in seconds): ')
  1087. print()
  1088.  
  1089. get_tout(tout)
  1090.  
  1091. if args.tcp: # or args.udp:
  1092. port = input('What port would you like to use to scan against? : ')
  1093. print()
  1094. else:
  1095. port = 0
  1096.  
  1097. if args.cidr or args.infile or args.host:
  1098. pass
  1099. else:
  1100. get_net_info()
  1101.  
  1102. if args.cidr:
  1103. cidr = input('What CIDR block would you like to use (use X.X.X.X/XXX format) : ')
  1104. # get_tout(tout)
  1105. print()
  1106. print('You chose CIDR block: ' + cidr)
  1107.  
  1108. if args.infile:
  1109. ifile = input('Please specify the explicit path to the file you want to import: ')
  1110. reader = csv.reader(open(ifile, 'r'))
  1111. state_dict = {}
  1112. for row in reader:
  1113. ip, rtt, count = row
  1114. if rtt == '':
  1115. rtt = 'None'
  1116. state_dict[ip] = [rtt, count]
  1117.  
  1118. if args.outfile:
  1119. ofile = input('Please specify the explicit path to the file you want to export the data to: ')
  1120.  
  1121. if args.host:
  1122. global host
  1123. host = input('What host would you like to scan (Use an IP in dotted decimal format X.X.X.X): ')
  1124. if not (args.tcp): # and not (args.udp):
  1125. rtt = ping(str(host), float(tout))
  1126. state_dict.update({host: [rtt, count]})
  1127. if args.tcp:
  1128. start = time.time()
  1129. tcp_scan(str(host), int(port))
  1130. if result != 0:
  1131. rtt = None
  1132. else:
  1133. rtt = time.time() - start
  1134. state_dict.update({host: [rtt, count]})
  1135. cidr = host + "/32"
  1136. else:
  1137. host = '0.0.0.0'
  1138.  
  1139. if args.email:
  1140. toaddrs = input('What email are you sending alerts to: ')
  1141. username = input('What is your gmail username: ')
  1142. password = input('What is your gmail password (you may need an application specific password): ')
  1143.  
  1144. if args.cidr or args.infile:
  1145. pass
  1146. else:
  1147. print_net_info(cidr, ip, dd_nm)
  1148.  
  1149. print()
  1150. input('Press Enter to start the scan')
  1151. if not args.infile:
  1152. os.system('clear')
  1153. initial_net_scan(cidr)
  1154. print_dict(state_dict)
  1155. if args.outfile:
  1156. csv_writer(ofile)
  1157. time.sleep(int(freq))
  1158.  
  1159. while True:
  1160. os.system('clear')
  1161. redundant_net_scan(state_dict, host, port)
  1162. print_dict(state_dict)
  1163. if args.outfile:
  1164. csv_writer(ofile)
  1165. time.sleep(int(freq))
  1166.  
  1167. except KeyboardInterrupt:
  1168. t2 = datetime.now()
  1169. timetotal = t2 - t1
  1170. print()
  1171. print("===================================================================================")
  1172. print()
  1173. print_dict(state_dict)
  1174. print()
  1175. print("===================================================================================")
  1176. print()
  1177. print("You pressed Ctrl+C")
  1178. print()
  1179. print("Your scan ran through " + str(totalruns) + " cycles, every " + str(freq) + " seconds.")
  1180. print()
  1181. print("The scan ran for a total of " + str(timetotal))
  1182. print()
  1183. print("The final data set is above:")
  1184. sys.exit()
  1185.  
  1186. except OSError as e:
  1187. t2 = datetime.now()
  1188. timetotal = t2 - t1
  1189. print()
  1190. print('Script crashed')
  1191. print('Dumping state dict')
  1192. print()
  1193. print('====================================================================================')
  1194. print()
  1195. print_dict(state_dict)
  1196. print()
  1197. print('====================================================================================')
  1198. print()
  1199. print('There was an OS Error exception, most likely a no route to host. for now, I'm just')
  1200. print('dumping the last version of the state dictionary and exiting.')
  1201. print()
  1202. print("Your scan ran through " + str(totalruns) + " cycles, every " + str(freq) + " seconds.")
  1203. print()
  1204. print("The scan ran for a total of " + str(timetotal))
  1205. print()
  1206. print('If you're seeing this error a lot, try changing the frequency to at least 30 seconds, and')
  1207. print('set the timeout to at least .1 for a trial. If it stops, you can tune it down. If it keeps')
  1208. print('failing, then you should increase both thresholds until it stops')
  1209.  
  1210. return count
  1211. return state_dict
  1212. return alert_total
  1213. return totalruns
  1214.  
  1215. return count, state,dict, alert_total, total_runs
  1216.  
  1217. def get_tout(a): # in this case, you never use the argument so you can get rid of it.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement