class PaloAltoAddress(BaseAddress): """Palo Alto Address Inherits from BaseAddress and provides access to the underlying pandevice object. Also provides and mapper for the type field. """ TypeMap = bidict({ BaseAddress.AddressTypes.IPv4: 'ip-netmask', BaseAddress.AddressTypes.RANGE: 'ip-range', BaseAddress.AddressTypes.DNS: 'fqdn', BaseAddress.AddressTypes.ANY: 'any' }) def __init__(self, pandevice_object): self.pandevice_object = pandevice_object super(PaloAltoAddress, self).__init__(name=pandevice_object.name, value=pandevice_object.value, a_type=PaloAltoAddress.TypeMap[pandevice_object.type]) @classmethod def from_criteria(cls, criteria): """Create an instance from the provided criteria """ pandevice_object = objects.AddressObject() pandevice_object.name = criteria['name'] pandevice_object.value = criteria['value'] pandevice_object.type = criteria['type'] return cls(pandevice_object)
class JuniperSRXPolicy(BasePolicy): """Juniper SRX Policy Inherits from Base Policy """ ActionMap = bidict({ BasePolicy.Action.ALLOW: 'permit', BasePolicy.Action.DENY: 'deny', BasePolicy.Action.REJECT: 'reject', }) LoggingMap = bidict({ BasePolicy.Logging.START: 'session-init', BasePolicy.Logging.END: 'session-close', }) def __init__(self, name, action, description, logging): super(JuniperSRXPolicy, self).__init__(name, action, description, logging) def to_xml(self): """Map Juniper SRX Policy Object into xml config element """ policy_element = create_element('policy') create_element('name', text=self.name, parent=policy_element) match_element = create_element('match', parent=policy_element) for s in self.src_addresses: create_element('source-address', text=s.name, parent=match_element) for d in self.dst_addresses: create_element('destination-address', text=d.name, parent=match_element) then_element = create_element('then', parent=policy_element) create_element(JuniperSRXPolicy.ActionMap[self.action], parent=then_element) log_element = create_element('log', parent=then_element) for log_type in self.logging: create_element(JuniperSRXPolicy.LoggingMap[log_type], parent=log_element) return policy_element
class BaseAddress(BaseObject): AddressTypes = enum('IPv4', 'DNS', 'RANGE', 'ANY') TypeMap = bidict({ AddressTypes.IPv4: 'ipv4', AddressTypes.DNS: 'dns', AddressTypes.RANGE: 'range', AddressTypes.ANY: 'any', }) def __init__(self, name, value, a_type): """init address object""" self.name = name self.value = value self.a_type = a_type def __getattr__(self, item): if item == 'value': return self.value else: raise AttributeError def table_value(self, with_names): if with_names: return self.name + " - " + self.value else: return self.value @classmethod def from_criteria(cls, criteria): """Create an instance from the provided criteria """ return cls(criteria['name'], criteria['value'], cls.TypeMap[criteria['type']]) def serialize(self): """Searialize self to a json acceptable data structure """ return { 'name': self.name, 'type': self.TypeMap[self.a_type], 'value': self.value, }
class PaloAltoApplication(BaseObject): """Palo Alto Appliciation """ IdentTypes = enum('PORT', 'PROTOCOL', 'ICMP', 'ICMP6') IdentTypeMap = bidict({ IdentTypes.PORT: 'port', IdentTypes.PROTOCOL: 'ident-by-ip-protocol', IdentTypes.ICMP: 'ident-by-icmp-type', IdentTypes.ICMP6: 'ident-by-icmp6-type', None: None, }) WellKnownServiceMap = bidict({ ('tcp', 80): 'web-browsing', ('tcp', 443): 'ssl', ('tcp', 22): 'ssh', ('udp', 123): 'ntp', ('tcp', 21): 'ftp', ('tcp', 23): 'telnet', ('tcp', 25): 'smtp', ('tcp', 1521): 'oracle', ('tcp', 1433): 'mssql-db', ('tcp', 445): 'ms-ds-smb', }) def __init__(self, pandevice_object): self.pandevice_object = pandevice_object self.name = self.pandevice_object.name self.default_type = PaloAltoApplication.IdentTypeMap[ self.pandevice_object.default_type] self.services = [] if self.default_type == PaloAltoApplication.IdentTypes.PORT: for p in self.pandevice_object.default_port: protocol = p.split('/')[0] port = p.split('/')[1] for port_element in port.split(','): self.services.append( BaseService(name="{0}-{1}".format( self.name, port_element), port=port_element, protocol=protocol)) def match_service(self, service_tuple): """Return true if self contains the given service, else false """ for service in self.services: if service.protocol == service_tuple[ 0] and service.port == service_tuple[1]: return True return False def __getattr__(self, item): if item == 'value': return self.name else: raise AttributeError def table_value(self): return self.name def serialize(self): """Searialize self to a json acceptable data structure """ services = [] for s in self.services: services.append(s.serialize()) return { 'name': self.name, 'default_type': self.default_type, 'services': services, }
class PaloAltoPolicy(BasePolicy): ActionMap = bidict({ BasePolicy.Action.ALLOW: 'allow', BasePolicy.Action.DROP: 'drop', BasePolicy.Action.DENY: 'deny' }) def __init__(self, pandevice_object=None): # this is the actual object provided by the 'pandevice' library self.pandevice_object = pandevice_object logging = [] if pandevice_object.log_start: logging.append(BasePolicy.Logging.START) if pandevice_object.log_end: logging.append(BasePolicy.Logging.END) self._applications = [] super(PaloAltoPolicy, self).__init__(name=pandevice_object.name, action=self.ActionMap[pandevice_object.action], description=pandevice_object.description, logging=logging) def add_application(self, app): self._applications.append(app) def serialize(self): """Searialize self to a json acceptable data structure """ # we need only update the super with local applications super_dict = super(PaloAltoPolicy, self).serialize() super_dict.update({ 'applications': list(map(lambda x: x.serialize(), self._applications)) }) return super_dict def __getattr__(self, item): """add applications access or call super""" if item == 'applications': return set(flatten([a.value for a in self._applications])) else: return super(PaloAltoPolicy, self).__getattr__(item) @staticmethod def table_service_cell(services, with_names=False): """handle the application-default case""" if services[0].name == "application-default": return "application-default\n" else: return super(PaloAltoPolicy, PaloAltoPolicy).table_service_cell( services, with_names=with_names) def table_application_cell(self): return "\n".join([a.table_value() for a in self._applications]) + '\n' def table_header(self): """Return the table header for the policy """ return [ "Src Zones", "Src Addresses", "Dst Zones", "Dst Addresses", "Applications", "Services", "Action" ] def table_row(self, with_names=False): """Return the table row for the based policy """ s_zones = self.table_zone_cell(self.src_zones) d_zones = self.table_zone_cell(self.dst_zones) s_addresses = self.table_address_cell(self.src_addresses, with_names) d_addresses = self.table_address_cell(self.dst_addresses, with_names) services = self.table_service_cell(self._services, with_names) applications = self.table_application_cell() return [ s_zones, s_addresses, d_zones, d_addresses, applications, services, self.ActionMap[self.action] ] @classmethod def from_criteria(cls, criteria): """Create an instance from the provided criteria """ logging_criteria = criteria.get('logging', []) pandevice_object = policies.SecurityRule() pandevice_object.name = criteria['name'] pandevice_object.description = criteria.get('description', '') pandevice_object.action = criteria[ 'action'] # we expect it to be human readable at this point, map it later if 'start' in logging_criteria or 'both' in logging_criteria: pandevice_object.log_start = True if 'end' in logging_criteria or 'both' in logging_criteria: pandevice_object.log_end = True return cls(pandevice_object)
class CandidatePolicy(BaseObject): """ candidate policy stores the target element(s) or new policy and a list of the best matched policies that can be appended to. """ # implementation methods Method = enum('NEW_POLICY', 'APPEND', 'TAG') MethodMap = bidict({ Method.NEW_POLICY: 'NEW_POLICY', Method.APPEND: 'APPEND', Method.TAG: 'TAG', }) def __init__(self, policy_criteria, matched_policies=None, method=Method.NEW_POLICY): self.policy_criteria = policy_criteria self.matched_policies = matched_policies or [] self.policy = None self.method = method self.tag_options = {} self.tag_choices = {} self.linked_objects = {} self.new_objects = {} # Palo Alto/Panorama specific params self.address_group_tag_options = {} self.shared_namespace = True self.context = None self.post_rulebase = True if self.method != self.Method.NEW_POLICY: # this will be an addendum to an existing policy default to the first policy self.set_base_policy(matched_policies[0]) def set_base_policy(self, policy): """Set or change the base policy""" if policy in self.matched_policies: self.policy = policy else: raise ValueError("Policy is not valid for this candidate policy") def serialize(self): """Serialize self into json compatible data structures """ linked_objects = {} for key, value in self.linked_objects.iteritems(): linked_objects[key] = {} for k, v in value.iteritems(): if key == 'services' and k != 'any': k = k[0] + "/" + k[1] if hasattr(v, 'serialize'): linked_objects[key][k] = v.serialize() else: linked_objects[key][k] = v new_objects = {} for key, value in self.new_objects.iteritems(): new_objects[key] = {} for k, v in value.iteritems(): if key == 'services' and k != 'any': k = k[0] + "/" + k[1] if hasattr(v, 'serialize'): new_objects[key][k] = v.serialize() else: new_objects[key][k] = v if self.context: context = self.context.device_group.name else: context = None if self.matched_policies: matched_policies = list( map(lambda x: x.serialize(), self.matched_policies)) else: matched_policies = [] if self.policy: policy = self.policy.serialize() else: policy = None return { 'policy_criteria': self.policy_criteria, 'matched_policies': matched_policies, 'policy': policy, 'method': self.MethodMap[self.method], 'tag_options': self.tag_options, 'tag_choices': self.tag_choices, 'linked_objects': linked_objects, 'new_objects': new_objects, 'context': context, 'address_group_tag_options': self.address_group_tag_options, 'shared_namespace': self.shared_namespace, 'post_rulebase': self.post_rulebase }
class BasePolicy(BaseObject): Action = enum('ALLOW', 'DENY', 'REJECT', 'DROP') Logging = enum('START', 'END') ActionMap = bidict({ Action.ALLOW: "Allow", Action.DENY: "Deny", Action.REJECT: "Reject", Action.DROP: 'Drop', }) def __init__(self, name, action, description, logging): """init policy""" self.name = name self.src_zones = list() self.dst_zones = list() self.src_addresses = list() self.dst_addresses = list() self._services = list() self.action = action self.description = description self.logging = logging def add_src_zone(self, zone): self.src_zones.append(zone) def add_dst_zone(self, zone): self.dst_zones.append(zone) def add_src_address(self, address): self.src_addresses.append(address) def add_dst_address(self, address): self.dst_addresses.append(address) def add_service(self, service): self._services.append(service) def serialize(self): """Searialize self to a json acceptable data structure """ s_addrs = list(map(lambda x: x.serialize(), self.src_addresses)) d_addrs = list(map(lambda x: x.serialize(), self.dst_addresses)) services = list(map(lambda x: x.serialize(), self._services)) return { 'name': self.name, 'source_zones': self.src_zones, 'source_addresses': s_addrs, 'destination_zones': self.dst_zones, 'destination_addresses': d_addrs, 'services': services, 'action': self.ActionMap[self.action], 'description': self.description, 'logging': self.logging } def __getattr__(self, item): """ return a tuple representation of the policy with normalized values """ # s_addrs = set(flatten([a.value for a in self.src_addresses])) # d_addrs = set(flatten([a.value for a in self.dst_addresses])) # services = set(flatten([s.value for s in self._services])) if item == 'value': # return self.src_zones, self.dst_zones, list(s_addrs), list(d_addrs), list(services), self.action pass # for use in policy match element reducing elif item == 'source_zones': return self.src_zones elif item == 'destination_zones': return self.dst_zones elif item == 'source_addresses': return set(flatten([a.value for a in self.src_addresses])) elif item == 'destination_addresses': return set(flatten([a.value for a in self.dst_addresses])) elif item == 'services': return set(flatten([s.value for s in self._services])) elif item == 'services_objects': return self._services else: raise AttributeError() @staticmethod def _in_network(value, p_value, exact_match=False): """ """ # 'any' address is an automatic match if we are exact if exact_match and 'any' in p_value: return True addresses = [ IPRange(a.split('-')[0], a.split('-')[1]) if '-' in a else IPNetwork(a) for a in value if is_ipv4(a) ] fqdns = [a for a in value if not is_ipv4(a)] p_addresses = [ IPRange(a.split('-')[0], a.split('-')[1]) if '-' in a else IPNetwork(a) for a in p_value if is_ipv4(a) ] p_fqdns = [a for a in p_value if not is_ipv4(a)] # network containment implies exact match... i think? for a in addresses: addr_result = any(a == b or a in b for b in p_addresses) if not addr_result: return False # now match the fqdns if exact_match: fqdn_result = set(fqdns) == set(p_fqdns) else: fqdn_result = set(p_fqdns).issubset(set(fqdns)) return fqdn_result def match(self, match_criteria, exact=False, match_containing_networks=True): """ determine if self is a match for the given criteria """ for key, value in match_criteria.iteritems(): if not value: # this key was included but has no value so skip it continue p_value = getattr(self, key, []) if p_value == 'any' and key in [ 'source_addresses', 'destination_addresses', 'services' ]: # 'any' as address constitutes a match, so move on continue elif key == 'action': # compare against the converted value if self.ActionMap[value] != p_value: return False elif len(value) > len(p_value): # more values in the match than the policy, fail return False elif match_containing_networks and key in [ 'source_addresses', 'destination_addresses' ]: if not self._in_network(value, p_value, exact_match=True): return False elif exact and not set(p_value) == set(value): return False elif not exact and not set(value).issubset(set(p_value)): return False return True def candidate_match(self, match_criteria, exact=False, match_containing_networks=True): """ wrap the match method in some extra logic to determine if this is a candidate for policy addition """ unique_key = None for key, value in match_criteria.iteritems(): match = self.match({key: value}, exact, match_containing_networks) if not match: if unique_key: # the unique key is already set so we fail return False else: # found a unique key unique_key = key # if we survived, this is a candidate policy so return which key is unique return unique_key @staticmethod def table_address_cell(addresses, with_names=False): return "\n".join([a.table_value(with_names) for a in addresses]) + '\n' @staticmethod def table_service_cell(services, with_names=False): return "\n".join([s.table_value(with_names) for s in services]) + '\n' @staticmethod def table_zone_cell(zones): return "\n".join([z for z in zones]) + '\n' def table_header(self): """Return the table header for the based policy """ return [ "Src Zones", "Src Addresses", "Dst Zones", "Dst Addresses", "Services", "Action" ] def table_row(self, with_names=False): """Return the table row for the based policy """ s_zones = self.table_zone_cell(self.src_zones) d_zones = self.table_zone_cell(self.dst_zones) s_addresses = self.table_address_cell(self.src_addresses, with_names) d_addresses = self.table_address_cell(self.dst_addresses, with_names) services = self.table_service_cell(self._services, with_names) return [ s_zones, s_addresses, d_zones, d_addresses, services, self.ActionMap[self.action] ] def to_table(self, with_names=False): """Return the policy as an ascii tables Args: with_names (bool): Include object names """ table_header = self.table_header() table_row = self.table_row(with_names=with_names) table = AsciiTable([table_header, table_row]) table.title = "Policy: " + self.name return table.table @classmethod def from_criteria(cls, criteria): """Create an instance from the provided criteria """ logging = [] logging_criteria = criteria.get('logging', []) if 'start' in logging_criteria or 'both' in logging_criteria: logging.append(cls.Logging.START) if 'end' in logging_criteria or 'both' in logging_criteria: logging.append(cls.Logging.END) return cls(criteria['name'], cls.ActionMap[criteria['action']], criteria.get('description'), logging)
class PaloAltoBaseDriver(BaseDriver): """Palo Alto Base class """ PolicyClass = PaloAltoPolicy ALLOWED_POLICY_KEYS = ( 'source_zones', 'destination_zones', 'source_addresses', 'destination_addresses', 'services', 'action', 'logging', 'applications', ) # in a given namespace there may be two related object types # here we map those together with a bidict NamespaceSisterTypes = bidict({ PaloAltoAddress: PaloAltoAddressGroup, PaloAltoService: PaloAltoServiceGroup, PaloAltoApplication: PaloAltoApplicationGroup, }) def open_connection(self, username, password, host, **kwargs): """ We need additional information for this driver """ # create a pan device object self.device = PanDevice.create_from_device(host, api_username=username, api_password=password, api_key=kwargs.get('apikey')) self._connected = True @staticmethod def tag_delta(expression, tag_list): """Take in a tag expression and a list of tags and give the delta of tags to meet the expression :return tuple( list( "required tags" ), list( "tuple of options" ) ) """ if tag_list is None: tag_list = [] required_tags = [] optional_tags = [] def parse_and(tokens): args = tokens[0][0::2] extend_list = filter(lambda x: isinstance(x, str) and x not in tag_list, args) required_tags.extend(extend_list) def parse_or(tokens): args = tokens[0][0::2] append_list = filter(lambda x: isinstance(x, str) and x not in tag_list, args) if append_list == args: optional_tags.append(tuple(append_list)) identifier = pp.Word(pp.alphanums + "_" + "-" + "'") expr = pp.infixNotation(identifier, [ ("AND", 2, pp.opAssoc.LEFT, parse_and), ("OR", 2, pp.opAssoc.LEFT, parse_or), ("and", 2, pp.opAssoc.LEFT, parse_and), ("or", 2, pp.opAssoc.LEFT, parse_or), ]) expr.parseString(expression) if expression and not required_tags and expression not in tag_list: # single tag in the expression required_tags.append(expression) return required_tags, optional_tags @staticmethod def _apply_object(_panobject, pandevice_object): """Apply (destructive) the given pandevice_object on the given pandevice """ _panobject.add(pandevice_object) pandevice_object.apply() @staticmethod def _create_object(_panobject, pandevice_object): """Create (non-destructive) the given pandevice_object on the given pandevice """ _panobject.add(pandevice_object) pandevice_object.create() @staticmethod def _link_policy_objects(pandevice_policy, objs, string_values): """Link the objects to the policy """ def _transform(l, v): transformed_value = l if v != 'any': # temporary fix until fixed in pandevice if l == ['any']: l = 'any' if isinstance(l, str): if l == 'any': transformed_value = [] else: transformed_value = [l] transformed_value.append(v) elif v not in l: transformed_value.append(v) else: transformed_value = v return transformed_value for key, value in objs.iteritems(): for obj in value.values(): if key == 'source_addresses': pandevice_policy.source = _transform(pandevice_policy.source, obj.name) elif key == 'destination_addresses': pandevice_policy.destination = _transform(pandevice_policy.destination, obj.name) elif key == 'applications': pandevice_policy.application = _transform(pandevice_policy.application, obj.name) elif key == 'services': pandevice_policy.service = _transform(pandevice_policy.service, obj.name) for key, value in string_values.iteritems(): if isinstance(value, list): # these keys are guaranteed to ne list values for lv in value: if key == 'source_zones': pandevice_policy.fromzone = _transform(pandevice_policy.fromzone, lv) elif key == 'destination_zones': pandevice_policy.tozone = _transform(pandevice_policy.tozone, lv) elif key == 'action': pandevice_policy.action = value elif key == 'name': pandevice_policy.name = value elif key == 'description': pandevice_policy.description = pandevice_policy.description + '\n' + value elif key == 'logging': if value == 'start' or value == 'both': pandevice_policy.log_start = True if value == 'end' or value == 'both': pandevice_policy.log_end = True @abc.abstractmethod def _get_config(self): # base and predefined refresh, this may take some time self.device.refresh() self.device.predefined.refreshall() @abc.abstractmethod def _parse_addresses(self): raise NotImplementedError() @abc.abstractmethod def _parse_address_groups(self): raise NotImplementedError() @abc.abstractmethod def _parse_services(self): raise NotImplementedError() @abc.abstractmethod def _parse_service_groups(self): raise NotImplementedError() @abc.abstractmethod def _parse_policies(self): raise NotImplementedError() @abc.abstractmethod def apply_candidate_policy(self, candidate_policy, commit=False): raise NotImplementedError() @abc.abstractmethod def apply_policy(self, policy, commit=False): raise NotImplementedError() @abc.abstractmethod def _parse_applications(self): raise NotImplementedError() @abc.abstractmethod def _parse_application_groups(self): raise NotImplementedError()