#!/usr/bin/python
'''grepacp - grep cisco acls for matching rules
options:
  -p <protocole>   one of ip, icmp, udp, tcp, or a number
  -s [ip address][:[port]]  full address, and/or port
  -d [ip address][:[port]]  full address, and/or port
  -a <regexp>    regular expression to match with the acl name, default "in"
  -r            reverse, check each file twice, once with src & dst reversed
  filename ...        name of acl file to grep
All arguments are optional
examples:
grepacl -r -s 130.194.6.27 -d 216.239.37.99:80 monash.acl
grepacl -s somehost:138 -d 130.194.148.18 sn148.acl sn048.acl 

$Id: grepacl,v 1.3 2005/09/21 13:42:12 kim Exp kim $
'''
copyright = '''
Copyright (C) 2005-2012, Kim Oldfield.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation, http://www.gnu.org/

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
'''

todo = '''
Warn if a netmask is not contiguous, eg 0.0.1.0
'''

import sys, re, struct, socket, fileinput, getopt

def hostname2ip(hostname):
	ip = socket.gethostbyname(hostname)
	if ip != hostname:
		print '%s => %s' % (hostname, ip)
	return ip

def ip2int(ip):
	# print 'ip:', ip
	if ip == '255.255.255.255':	# inet_aton dies on this
		return 0xffffffff
	return struct.unpack('>L', socket.inet_aton(ip))[0]

def int2ip(n):
	return socket.inet_ntoa(struct.pack('>l', n))

def tomask(s):
	'''
	converts a cisco acl definition for:
	 - host xxx,
	 - any, or
	 - xxx yyy (wildcard mask)
	to an (ip, netmask) pair
	'''
	if s == 'any':
		return 0, 0
	a, b = s.split(None, 1)
	if a == 'host':
		return ip2int(b), 0xffffffff
	return ip2int(a), ~ ip2int(b)

def readservices():
	dict = {}
	for line in fileinput.input('/etc/services'):
		if re.search(r'^\s*(\#.*)?$', line):
			# skip comment only or blank line
			continue
		m = re.search(r'''^(?P<service>\S+)
			\s+(?P<port>\d+)\/(tcp|udp)
			(?P<alias>(\s+\S+)*)
			(?P<comment>\s+\#.*)?
			\s*$''',
			line, re.VERBOSE)
		if m:
			dict[m.group('service')] = int(m.group('port'))
			if m.group('alias'):
				aliases = m.group('alias').split()
				for i in aliases:
					dict[i] = int(m.group('port'))
		# ignore unmatched lines
		#else:
		#	print 'Ignoring service line:', `line`
	return dict

services = readservices()
def port2int(s):
	if services.has_key(s):
		return services[s]
	try:
		return int(s)
	except ValueError:
		print ' Unreconised port', `s`
		return -1

def torange(s):
	'''
	converts a cisco acl port specification:
	  - eq
	  - lt
	  - gt
	  - range x y
	to a port range (lower, upper)
	'''
	if s == None:	# no restriction => allports
		return 0, 0xffff
	list = s.split()
	if list[0] == 'eq':
		return port2int(list[1]), port2int(list[1])
	if list[0] == 'lt':
		return 0, port2int(list[1])
	if list[0] == 'gt':
		return port2int(list[1]), 0xffff
	if list[0] == 'range':
		return port2int(list[1]), port2int(list[2])
	print 'Unrecognised port defintion:', s
	return 0, 0xffff

def checkacl(filename, proto, src, dst, srcport, dstport, aclname):
	if 0:
		print 'checkacl(%s, %s, %s, %s, %s, %s, %s)' % (`filename`, `proto`, `src`, `dst`, `srcport`, `dstport`, `aclname`)
	inacl = (aclname == None)
	aclre = re.compile(r'''^(?P<pd>permit|deny)
		\s+(?P<proto>ip|icmp|tcp|udp|gre|\d+)
		\s+(?P<src>any
			|host\s+\d+(\.\d+){3}
			|\d+(\.\d+){3}\s+\d+(\.\d+){3}
			)
		(\s+(?P<srcport>range\s+[-\w]+\s+[-\w]+|(eq|gt|lt)\s+\w+))?
		\s+(?P<dst>any
			|host\s+\d+(\.\d+){3}
			|\d+(\.\d+){3}\s+\d+(\.\d+){3}
			)
		(\s+(?P<dstport>range\s+[-\w]+\s+[-\w]+|(eq|gt|lt)\s+\w+))?
		(\s+(?P<mod>established|log|log-input
			|echo(-reply)?|unreachable
			))*
		$''', re.VERBOSE)
	try:
		for line in fileinput.input(filename):
			# stip comments and leading or trailing whitespace
			m = re.search(r'^\s*([^\!]*?)\s*((\!|\bremark\b).*)?$', line)
			if not m:
				print 'Failed to match in:', line
				return
			line = m.group(1)
			if not line:
				continue	# ignore empty lines
			if re.search(r'^no\s+ip\s+access-list\s+', line):
				continue
			if re.search(r'^(no\s+)?ip\s+access-group\s+', line):
				continue
			if re.search(r'^interface\s+', line):
				continue
			m = re.search(r'^ip\s+access-list(\s+extended)?\s+([-\w]+)$', line)
			if m:
				if aclname and re.search(aclname, m.group(2)):
					print line
					inacl = 1
				continue
			if re.search(r'^exit|end$', line):
				if aclname:
					inacl = 0
				continue
			m = aclre.search(line)
			if not m:
				print '  unknown line:', `line`
				continue
			if not inacl:
				continue
			if proto and proto != 'ip':
				if proto == 'commonip':
					if m.group('proto') not in ('ip', 'udp', 'tcp', 'icmp'):
						continue
				elif m.group('proto') != 'ip':
					if proto != m.group('proto'):
						continue
				#else:
				#	if proto not in ('ip', 'udp', 'tcp', 'udp', 'icmp'):
				#		continue
			if src:
				aclsrc, aclsrcmask = tomask(m.group('src'))
				if src & aclsrcmask != aclsrc & aclsrcmask:
					continue
			if srcport:
				portlow, porthigh = torange(m.group('srcport'))
				if not (portlow <= srcport <= porthigh):
					continue
			if dst:
				acldst, acldstmask = tomask(m.group('dst'))
				if dst & acldstmask != acldst & acldstmask:
					continue
			if dstport:
				portlow, porthigh = torange(m.group('dstport'))
				if not (portlow <= dstport <= porthigh):
					continue
			# this line matched
			print ' ', line
	except IOError:
		print 'IOError attempting to read file', `filename`

def swapinout(aclname):
	if aclname and aclname.find('in') != -1:
		return aclname.replace('in', 'out')
	elif aclname and aclname.find('out') != -1:
		return aclname.replace('out', 'in')

def main():
	if len(sys.argv) <= 1:
		print __doc__
		return
	proto = None
	src = None
	srcport = None
	dst = None
	dstport = None
	aclname = 'in'
	reverse = 1
	optlist, aclfiles = getopt.getopt(sys.argv[1:], 'p:s:d:a:r')
	for arg, val in optlist:
		if arg == '-p':
			proto = val
		elif arg == '-s':
			a = val
			try:
				b, c = a.split(':', 1)
				if b:	src = b
				if c:	srcport = c
			except ValueError:	# no ':'
				src = a
			if src: src = hostname2ip(src)
		elif arg == '-d':
			a = val
			try:
				b, c = val.split(':', 1)
				if b:	dst = b
				if c:	dstport = c
			except ValueError:	# no ':'
				dst = a
			if dst: dst = hostname2ip(dst)
		elif arg == '-a':
			aclname = val
		elif arg == '-r':
			reverse = not reverse
		else:
			print 'Unknown option:', arg, val

	if src:		src = ip2int(src)
	if dst:		dst = ip2int(dst)
	if srcport:	srcport = int(srcport)
	if dstport:	dstport = int(dstport)

	#initialaclname = aclname
	for filename in aclfiles:
		print 'file:', filename
		checkacl(filename, proto, src=src, dst=dst, srcport=srcport, dstport=dstport, aclname=aclname)
		if reverse:
			aclname = swapinout(aclname)
	if reverse:
		print
		print 'Reversing direction:'
		src, dst = dst, src
		srcport, dstport = dstport, srcport
		aclfiles.reverse()
		# file order has been reversed, so don't need to reverse acl direction
		#aclname = swapinout(initialaclname)
		for filename in aclfiles:
			print 'file:', filename
			checkacl(filename, proto, src=src, dst=dst, srcport=srcport, dstport=dstport, aclname=aclname)
			aclname = swapinout(aclname)

if __name__ == '__main__':
	main()
