diff options
Diffstat (limited to 'lib/naming.py')
-rw-r--r-- | lib/naming.py | 502 |
1 files changed, 502 insertions, 0 deletions
diff --git a/lib/naming.py b/lib/naming.py new file mode 100644 index 0000000..40196bc --- /dev/null +++ b/lib/naming.py @@ -0,0 +1,502 @@ +#!/usr/bin/python +# +# Copyright 2011 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Parse naming definition files. + +Network access control applications use definition files which contain +information about networks and services. This naming class +will provide an easy interface into using these definitions. + +Sample usage with definition files contained in ./acl/defs: + defs = Naming('acl/defs/') + + services = defs.GetService('DNS') + returns ['53/tcp', '53/udp', ...] + + networks = defs.GetNet('INTERNAL') + returns a list of nacaddr.IPv4 object + +The definition files are contained in a single directory and +may consist of multiple files ending in .net or .svc extensions, +indicating network or service definitions respectively. The +format of the files consists of a 'token' value, followed by a +list of values and optional comments, such as: + +INTERNAL = 10.0.0.0/8 # RFC-1918 + 172.16.0.0/12 # RFC-1918 + 192.168.0.0/16 # RFC-1918 +or + +DNS = 53/tcp + 53/udp + +""" + +__author__ = 'watson@google.com (Tony Watson)' + +import glob + +import nacaddr + + +class Error(Exception): + """Create our own base error class to be inherited by other error classes.""" + + +class NamespaceCollisionError(Error): + """Used to report on duplicate symbol names found while parsing.""" + + +class BadNetmaskTypeError(Error): + """Used to report on duplicate symbol names found while parsing.""" + + +class NoDefinitionsError(Error): + """Raised if no definitions are found.""" + + +class ParseError(Error): + """Raised if an error occurs during parsing.""" + + +class UndefinedAddressError(Error): + """Raised if an address is referenced but not defined.""" + + +class UndefinedServiceError(Error): + """Raised if a service is referenced but not defined.""" + + +class UnexpectedDefinitionType(Error): + """An unexpected/unknown definition type was used.""" + + +class _ItemUnit(object): + """This class is a container for an index key and a list of associated values. + + An ItemUnit will contain the name of either a service or network group, + and a list of the associated values separated by spaces. + + Attributes: + name: A string representing a unique token value. + items: a list of strings containing values for the token. + """ + + def __init__(self, symbol): + self.name = symbol + self.items = [] + + +class Naming(object): + """Object to hold naming objects from NETWORK and SERVICES definition files. + + Attributes: + current_symbol: The current token being handled while parsing data. + services: A collection of all of the current service item tokens. + networks: A collection of all the current network item tokens. + """ + + def __init__(self, naming_dir=None, naming_file=None, naming_type=None): + """Set the default values for a new Naming object.""" + self.current_symbol = None + self.services = {} + self.networks = {} + self.unseen_services = {} + self.unseen_networks = {} + if naming_file and naming_type: + filename = os.path.sep.join([naming_dir, naming_file]) + file_handle = gfile.GFile(filename, 'r') + self._ParseFile(file_handle, naming_type) + elif naming_dir: + self._Parse(naming_dir, 'services') + self._CheckUnseen('services') + + self._Parse(naming_dir, 'networks') + self._CheckUnseen('networks') + + def _CheckUnseen(self, def_type): + if def_type == 'services': + if self.unseen_services: + raise UndefinedServiceError('%s %s' % ( + 'The following tokens were nested as a values, but not defined', + self.unseen_services)) + if def_type == 'networks': + if self.unseen_networks: + raise UndefinedAddressError('%s %s' % ( + 'The following tokens were nested as a values, but not defined', + self.unseen_networks)) + + def GetIpParents(self, query): + """Return network tokens that contain IP in query. + + Args: + query: an ip string ('10.1.1.1') or nacaddr.IP object + """ + base_parents = [] + recursive_parents = [] + # convert string to nacaddr, if arg is ipaddr then convert str() to nacaddr + if type(query) != nacaddr.IPv4 and type(query) != nacaddr.IPv6: + if query[:1].isdigit(): + query = nacaddr.IP(query) + # Get parent token for an IP + if type(query) == nacaddr.IPv4 or type(query) == nacaddr.IPv6: + for token in self.networks: + for item in self.networks[token].items: + item = item.split('#')[0].strip() + if item[:1].isdigit() and nacaddr.IP(item).Contains(query): + base_parents.append(token) + # Get parent token for another token + else: + for token in self.networks: + for item in self.networks[token].items: + item = item.split('#')[0].strip() + if item[:1].isalpha() and item == query: + base_parents.append(token) + # look for nested tokens + for bp in base_parents: + done = False + for token in self.networks: + if bp in self.networks[token].items: + # ignore IPs, only look at token values + if bp[:1].isalpha(): + if bp not in recursive_parents: + recursive_parents.append(bp) + recursive_parents.extend(self.GetIpParents(bp)) + done = True + # if no nested tokens, just append value + if not done: + if bp[:1].isalpha() and bp not in recursive_parents: + recursive_parents.append(bp) + return sorted(list(set(recursive_parents))) + + def GetServiceParents(self, query): + """Given a query token, return list of services definitions with that token. + + Args: + query: a service token name. + """ + return self._GetParents(query, self.services) + + def GetNetParents(self, query): + """Given a query token, return list of network definitions with that token. + + Args: + query: a network token name. + """ + return self._GetParents(query, self.networks) + + def _GetParents(self, query, query_group): + """Given a naming item dict, return any tokens containing the value. + + Args: + query: a service or token name, such as 53/tcp or DNS + query_group: either services or networks dict + """ + base_parents = [] + recursive_parents = [] + # collect list of tokens containing query + for token in query_group: + if query in query_group[token].items: + base_parents.append(token) + if not base_parents: + return [] + # iterate through tokens containing query, doing recursion if necessary + for bp in base_parents: + for token in query_group: + if bp in query_group[token].items and bp not in recursive_parents: + recursive_parents.append(bp) + recursive_parents.extend(self._GetParents(bp, query_group)) + if bp not in recursive_parents: + recursive_parents.append(bp) + return recursive_parents + + def GetService(self, query): + """Given a service name, return a list of associated ports and protocols. + + Args: + query: Service name symbol or token. + + Returns: + A list of service values such as ['80/tcp', '443/tcp', '161/udp', ...] + + Raises: + UndefinedServiceError: If the service name isn't defined. + """ + expandset = set() + already_done = set() + data = [] + service_name = '' + data = query.split('#') # Get the token keyword and remove any comment + service_name = data[0].split()[0] # strip and cast from list to string + if service_name not in self.services: + raise UndefinedServiceError('\nNo such service: %s' % query) + + already_done.add(service_name) + + for next_item in self.services[service_name].items: + # Remove any trailing comment. + service = next_item.split('#')[0].strip() + # Recognized token, not a value. + if not '/' in service: + # Make sure we are not descending into recursion hell. + if service not in already_done: + already_done.add(service) + try: + expandset.update(self.GetService(service)) + except UndefinedServiceError as e: + # One of the services in query is undefined, refine the error msg. + raise UndefinedServiceError('%s (in %s)' % (e, query)) + else: + expandset.add(service) + return sorted(expandset) + + def GetServiceByProto(self, query, proto): + """Given a service name, return list of ports in the service by protocol. + + Args: + query: Service name to lookup. + proto: A particular protocol to restrict results by, such as 'tcp'. + + Returns: + A list of service values of type 'proto', such as ['80', '443', ...] + + Raises: + UndefinedServiceError: If the service name isn't defined. + """ + services_set = set() + proto = proto.upper() + data = [] + servicename = '' + data = query.split('#') # Get the token keyword and remove any comment + servicename = data[0].split()[0] # strip and cast from list to string + if servicename not in self.services: + raise UndefinedServiceError('%s %s' % ('\nNo such service,', servicename)) + + for service in self.GetService(servicename): + if service and '/' in service: + parts = service.split('/') + if parts[1].upper() == proto: + services_set.add(parts[0]) + return sorted(services_set) + + def GetNetAddr(self, token): + """Given a network token, return a list of netaddr.IPv4 objects. + + Args: + token: A name of a network definition, such as 'INTERNAL' + + Returns: + A list of netaddr.IPv4 objects. + + Raises: + UndefinedAddressError: if the network name isn't defined. + """ + return self.GetNet(token) + + def GetNet(self, query): + """Expand a network token into a list of nacaddr.IPv4 objects. + + Args: + query: Network definition token which may include comment text + + Raises: + BadNetmaskTypeError: Results when an unknown netmask_type is + specified. Acceptable values are 'cidr', 'netmask', and 'hostmask'. + + Returns: + List of nacaddr.IPv4 objects + + Raises: + UndefinedAddressError: for an undefined token value + """ + returnlist = [] + data = [] + token = '' + data = query.split('#') # Get the token keyword and remove any comment + token = data[0].split()[0] # Remove whitespace and cast from list to string + if token not in self.networks: + raise UndefinedAddressError('%s %s' % ('\nUNDEFINED:', str(token))) + + for next in self.networks[token].items: + comment = '' + if next.find('#') > -1: + (net, comment) = next.split('#', 1) + else: + net = next + try: + net = net.strip() + addr = nacaddr.IP(net) + # we want to make sure that we're storing the network addresses + # ie, FOO = 192.168.1.1/24 should actually return 192.168.1.0/24 + if addr.ip != addr.network: + addr = nacaddr.IP('%s/%d' % (addr.network, addr.prefixlen)) + + addr.text = comment.lstrip() + addr.token = token + returnlist.append(addr) + except ValueError: + # if net was something like 'FOO', or the name of another token which + # needs to be dereferenced, nacaddr.IP() will return a ValueError + returnlist.extend(self.GetNet(net)) + for next in returnlist: + next.parent_token = token + return returnlist + + def _Parse(self, defdirectory, def_type): + """Parse files of a particular type for tokens and values. + + Given a directory name and the type (services|networks) to + process, grab all the appropriate files in that directory + and parse them for definitions. + + Args: + defdirectory: Path to directory containing definition files. + def_type: Type of definitions to parse + + Raises: + NoDefinitionsError: if no definitions are found. + """ + file_names = [] + get_files = {'services': lambda: glob.glob(defdirectory + '/*.svc'), + 'networks': lambda: glob.glob(defdirectory + '/*.net')} + + if def_type in get_files: + file_names = get_files[def_type]() + else: + raise NoDefinitionsError('Unknown definitions type.') + if not file_names: + raise NoDefinitionsError('No definition files for %s in %s found.' % + (def_type, defdirectory)) + + for current_file in file_names: + try: + file_handle = open(current_file, 'r').readlines() + for line in file_handle: + self._ParseLine(line, def_type) + except IOError as error_info: + raise NoDefinitionsError('%s', error_info) + + def _ParseFile(self, file_handle, def_type): + for line in file_handle: + self._ParseLine(line, def_type) + + def ParseServiceList(self, data): + """Take an array of service data and import into class. + + This method allows us to pass an array of data that contains service + definitions that are appended to any definitions read from files. + + Args: + data: array of text lines containing service definitions. + """ + for line in data: + self._ParseLine(line, 'services') + + def ParseNetworkList(self, data): + """Take an array of network data and import into class. + + This method allows us to pass an array of data that contains network + definitions that are appended to any definitions read from files. + + Args: + data: array of text lines containing net definitions. + + """ + for line in data: + self._ParseLine(line, 'networks') + + def _ParseLine(self, line, definition_type): + """Parse a single line of a service definition file. + + This routine is used to parse a single line of a service + definition file, building a list of 'self.services' objects + as each line of the file is iterated through. + + Args: + line: A single line from a service definition files. + definition_type: Either 'networks' or 'services' + + Raises: + UnexpectedDefinitionType: when called with unexpected type of defintions + NamespaceCollisionError: when overlapping tokens are found. + ParseError: If errors occur + """ + if definition_type not in ['services', 'networks']: + raise UnexpectedDefinitionType('%s %s' % ( + 'Received an unexpected defintion type:', definition_type)) + line = line.strip() + if not line or line.startswith('#'): # Skip comments and blanks. + return + comment = '' + if line.find('#') > -1: # if there is a comment, save it + (line, comment) = line.split('#', 1) + line_parts = line.split('=') # Split on var = val lines. + # the value field still has the comment at this point + # If there was '=', then do var and value + if len(line_parts) > 1: + self.current_symbol = line_parts[0].strip() # varname left of '=' + if definition_type == 'services': + if self.current_symbol in self.services: + raise NamespaceCollisionError('%s %s' % ( + '\nMultiple definitions found for service: ', + self.current_symbol)) + elif definition_type == 'networks': + if self.current_symbol in self.networks: + raise NamespaceCollisionError('%s %s' % ( + '\nMultiple definitions found for service: ', + self.current_symbol)) + + self.unit = _ItemUnit(self.current_symbol) + if definition_type == 'services': + self.services[self.current_symbol] = self.unit + # unseen_services is a list of service TOKENS found in the values + # of newly defined services, but not previously defined themselves. + # When we define a new service, we should remove it (if it exists) + # from the list of unseen_services. + if self.current_symbol in self.unseen_services: + self.unseen_services.pop(self.current_symbol) + elif definition_type == 'networks': + self.networks[self.current_symbol] = self.unit + if self.current_symbol in self.unseen_networks: + self.unseen_networks.pop(self.current_symbol) + else: + raise ParseError('Unknown definitions type.') + values = line_parts[1] + # No '=', so this is a value only line + else: + values = line_parts[0] # values for previous var are continued this line + for value_piece in values.split(): + if not value_piece: + continue + if not self.current_symbol: + break + if comment: + self.unit.items.append(value_piece + ' # ' + comment) + else: + self.unit.items.append(value_piece) + # token? + if value_piece[0].isalpha() and ':' not in value_piece: + if definition_type == 'services': + # already in top definitions list? + if value_piece not in self.services: + # already have it as an unused value? + if value_piece not in self.unseen_services: + self.unseen_services[value_piece] = True + if definition_type == 'networks': + if value_piece not in self.networks: + if value_piece not in self.unseen_networks: + self.unseen_networks[value_piece] = True |