import csv, os
from netcrawl import io_sql, util, config
import textwrap
from netcrawl.io_sql import device_db
from netcrawl.tools.manuf.manuf import MacParser
from prettytable import PrettyTable
def _open_csv(_path):
'''File must have headers, and it must include the following:
: 'mac' - Contains the MAC's to check
: 'ip' - Ip address of the network
: 'subnet' - Mask for the network
The csv will be parsed for mac addresses that resemble those
in the report'''
if not os.path.isfile(_path):
raise IOError('File does not exist [{}]'.format(_path))
with open(_path) as csvfile:
cr= csv.DictReader(csvfile)
entries= []
# Iterate over the csv
for row in cr:
assert 'ip' in row, 'CSV had no IP'
assert 'mac' in row, 'CSV had no MAC'
# Make sure it has the right headers
if all (k in row for k in ('ip', 'mac', 'subnet')):
# Sanitize the input
row['ip'] = util.clean_ip(row['ip'])
row['subnet'] = util.clean_ip(row['subnet'])
row['mac'] = util.ucase_letters(row['mac'])
row['network_ip']= util.network_ip(
row['ip'],
row['subnet'])
entries.append(row)
return entries
[docs]def run_audit(csv_path):
'''
Given a CSV of subnets and MAC addresses, search the database
for all MACs on subnets which match those in the CSV. Compare
each MAC and output a new csv with any matching MAC's listed
by confidence (number of matching characters, starting from the
OUI.
This can be used, for example, for a Wireless Rogue SSID audit,
for which the MAC address of the radios is known and you want to
find out which rogue AP's are physically connected to your network.
'''
if config.cc.modified is False:
config.parse_config()
# Open the input CSV
entries= _open_csv(csv_path)
csv_subnets= sort_csv_by_subnet(entries)
print ('CSV Len: ', len(csv_subnets))
device_db = io_sql.device_db()
results=[]
mp = MacParser(update=True)
# Iterate over each subnet where a rogue was detected
for subnet in sorted(csv_subnets):
print('Subnet: ', subnet)
# Iterate over each mac in the subnet
for mac in device_db.macs_on_subnet(subnet):
# Iterate over each mac in the CSV subnet and
# find matches
for csv_row in csv_subnets[subnet]:
x= evaluate_mac(mac, csv_row['mac'])
if x > 50:
csv_row= dict(csv_row)
csv_row['confidence'] = x
csv_row['wired_mac'] = mac
csv_row['Manufacturer'] = mp.search(mac)
results.append(csv_row)
results= sorted(results, key=lambda x: x['confidence'], reverse=True)
if len(results) == 0: return False
write_csv(results)
write_report(results)
[docs]def write_report(rows):
from datetime import datetime
ddb= device_db()
path= os.path.join(config.cc.run_path, 'mac_audit_report.txt')
with open(path, 'w') as outfile:
outfile.write(textwrap.dedent('''\
Title: Rogue Device Report
Time: {}
Note: In the neighbor table for each match, the interface with no
neighbor or a neighbor which is an obvious non-network
device (such as a phone) is the most likely interface to be
directly connected to the matched MAC.
'''.format(datetime.now().strftime(config.cc.pretty_time))))
for x in rows:
# Get the neighbors
locations= ddb.locate_mac(x['wired_mac'])
result= '\n\n'
result+= '{:12}: {}\n'.format('Matched Mac', x.pop('mac'))
result+= '{:12}: {}\n'.format('Wired Mac', x.pop('wired_mac'))
result+= '{:12}: {}\n'.format('Confidence', x.pop('confidence'))
result+= '{:12}: {}\n'.format('Manufacturer', x.pop('Manufacturer'))
result+= '\n'.join(['{:12}: {}'.format(k, v) for k, v in sorted(x.items())])
result+= '\n\nWhere this MAC was seen:\n'
t = PrettyTable(['Device Name', 'Interface', 'Neighbors'])
t.align = 'l'
for match in locations: t.add_row(match)
result+= str(t) + '\n'
outfile.write(result)
print('Finished writing report to [{}]'.format(path))
[docs]def write_csv(rows):
path= os.path.join(config.cc.run_path, 'mac_audit.csv')
with open(path, 'w', newline='') as outfile:
keys= [k for k, v in rows[0].items()]
writer = csv.DictWriter(outfile, fieldnames=keys)
writer.writeheader()
for x in rows:
writer.writerow(x)
print('Finished writing CSV to [{}]'.format(path))
[docs]def sort_csv_by_subnet(csv_rows):
'''Takes a list of dicts with 'network_ip' and 'mac'
as keys, then produces a dict of lists containing
subnets and the mac addresses associated with them'''
subnets={}
for row in csv_rows:
if row['network_ip'] not in subnets:
subnets[row['network_ip']] = []
subnets[row['network_ip']].append(row)
return subnets
[docs]def evaluate_mac(mac1, mac2):
if any([mac1 is None,
mac2 is None]):
return 0
# Strip all non-letter characters
mac1= util.ucase_letters(mac1)
mac2= util.ucase_letters(mac2)
if len(mac1) != len(mac2):
return 0
count=0
for i in range(len(mac1)):
if mac1[i] == mac2[i]: count+=1
# Break at the first bad match
else: break
#===========================================================================
# # Use this to return the exact number of characters matched
# return count
#===========================================================================
# Returns a percentage match
if count==0: return 0
return int((count / len(mac1)) * 100)
[docs]def main():
'''
Begins the audit. Outputs a csv file containing the audit results.
'''
import argparse
config.parse_config()
parser = argparse.ArgumentParser(description='Perform an audit of MACs on the network')
parser.add_argument('csv', help='A csv file to audit.')
args = parser.parse_args()
run_audit(args.csv)
if __name__ == '__main__':
main()