# Copyright (c) 2017, Manito Networks, LLC # All rights reserved. ### Packed Integer Parsers ### class name_lookups(object): import socket, time from site_category import site_categories dns_cache = {} # Name cache # Multicast per http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml special_ips = { "255.255.255.255":"Broadcast", "127.0.0.1":"Localhost", "224.0.0.0":"Multicast", "224.0.0.1":"All Hosts Multicast", "224.0.0.2":"All Routers Multicast", "224.0.0.4":"DVMRP Multicast", "224.0.0.5":"OSPF Router Multicast", "224.0.0.6":"OSPF Designated Router Multicast", "224.0.0.9":"RIPv2 Multicast", "224.0.0.10":"EIGRP Multicast", "224.0.0.12":"DHCP Server / Relay", "224.0.0.13":"PIMv2 Multicast", "224.0.0.14":"RSVP Encapsulation Multicast", "224.0.0.18":"VRRP Multicast", "224.0.0.19":"IS-IS Multicast", "224.0.0.20":"IS-IS Multicast", "224.0.0.21":"IS-IS Multicast", "224.0.0.22":"IGMPv3 Multicast", "224.0.0.102":"HSRPv2 Multicast", "224.0.0.252":"Link-local Multicast Name Resolution", "224.0.0.252":"Teredo Multicast" } # Add special IPs to dns_cache for key in special_ips: dns_cache[key] = {} dns_cache[key]["FQDN"] = special_ips[key] dns_cache[key]["Domain"] = special_ips[key] dns_cache[key]["Content"] = "Uncategorized" second_level_domains = { "co.id", # Indonesia "co.in", # India "co.jp", # Japan "co.nz", # New Zealand "co.uk", # United Kingdom "co.za", # South Africa "com.ar", # Argentina "com.au", # Australia "com.bn", # Brunei "com.br", # Brazil "com.cn", # People's Republic of China "com.gh", # Ghana "com.hk", # Hong Kong "com.mx", # Mexico "com.sg", # Singapore "edu.au", # Australia "net.au", # Australia "net.il", # Israel "org.au" # Australia } def __init__(self): return def ip_names( self, ip_version, # type: int ip_addr # type: str ): """ DNS reverse lookups for SRC and DST IP addresses Args: ip_version (int): Internet Protocol version ip_addr (str): IP address for lookup Returns: dict: {Content, Domain, Expires, FQDN} """ #if ip_version == 4: # IPv4 lookups if ip_addr not in self.dns_cache: # Not already cached # Record cache self.dns_cache[ip_addr] = {} self.dns_cache[ip_addr]["Expires"] = int(self.time.time())+1800 try: ip_lookup = str(self.socket.getfqdn(ip_addr)) # Run reverse lookup except Exception as lookup_message: # DNS lookup failed print(lookup_message) return False # Successful lookup if ip_lookup != ip_addr: # Update the local cache self.dns_cache[ip_addr]["FQDN"] = ip_lookup # FQDN # Parse the FQDN for Domain information if "." in ip_lookup: fqdn_exploded = ip_lookup.split('.') # Blow it up # Grab TLD and second-level domain domain = str(fqdn_exploded[-2]) + "." + str(fqdn_exploded[-1]) # Check for .co.uk, .com.jp, etc... if domain in self.second_level_domains: domain = str(fqdn_exploded[-3]) + "." + str(domain) # Hostname, no domain else: domain = ip_lookup self.dns_cache[ip_addr]["Domain"] = domain # Domain if self.dns_cache[ip_addr]["Domain"] in self.site_categories: # Documented site with Content self.dns_cache[ip_addr]["Content"] = self.site_categories[self.dns_cache[ip_addr]["Domain"]] else: self.dns_cache[ip_addr]["Content"] = "Uncategorized" # Default content; Normalize graphs # No DNS record, use IP instead else: self.dns_cache[ip_addr]["FQDN"] = ip_addr # Normalize graphs self.dns_cache[ip_addr]["Domain"] = ip_addr # Normalize graphs self.dns_cache[ip_addr]["Content"] = "Uncategorized" # Default content; Normalize graphs return self.dns_cache[ip_addr] ### Packed Integer Parsers ### class int_parse(object): from struct import unpack def __init__(self): return def integer_unpack( self, packed_data, # type: struct pointer, # type: int field_size # type: int ): """ Unpack an Integer Args: packed_data (struct): Packed data pointer (int): Current unpack location field_size (int): Length of data to unpack Returns: str: IPv4 address """ if field_size == 1: return self.unpack('!B',packed_data[pointer:pointer+field_size])[0] elif field_size == 2: return self.unpack('!H',packed_data[pointer:pointer+field_size])[0] elif field_size == 4: return self.unpack('!I',packed_data[pointer:pointer+field_size])[0] elif field_size == 8: return self.unpack('!Q',packed_data[pointer:pointer+field_size])[0] else: return False ### Packed IP Parsers (Netflow v5, Netflow v9, IPFIX) ### class ip_parse(object): import socket # Windows socket.inet_ntop support via win_inet_pton try: import win_inet_pton except ImportError: pass def __init__(self): return # Unpack IPv4 def parse_ipv4( self, packed_data, # type: struct pointer, # type: int field_size # type: int ): """ Unpack an IPv4 address Args: packed_data (struct): Packed data pointer (int): Current unpack location field_size (int): Length of data to unpack Returns: str: IPv4 address """ payload = self.socket.inet_ntoa(packed_data[pointer:pointer+field_size]) return payload # Unpack IPv6 def parse_ipv6( self, packed_data, # type: struct pointer, # type: int field_size # type: int ): """ Unpack an IPv6 address Args: packed_data (struct): Packed data pointer (int): Current unpack location field_size (int): Length of data to unpack Returns: str: IPv4 address """ payload = self.socket.inet_ntop(self.socket.AF_INET6,packed_data[pointer:pointer+field_size]) return payload ### Generic MAC Address Parsers ### class mac_address(object): import struct def __init__(self): # See the following for MAC OUI information: # http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml # https://tools.ietf.org/html/rfc7042 mac_oui = { "005056": {"Vendor":"VMware", "Type":"Virtualization"}, "00005E": {"Vendor":"IANA", "Type":"Unicast"}, "01000C": {"Vendor":"Cisco", "Type":"Logical"}, "01005E": {"Vendor":"IANA", "Type":"Multicast"}, "0180C2": {"Vendor":"IEEE", "Type":"Logical"}, "0A0027": {"Vendor":"Oracle", "Type":"Virtualization"}, "2C0E3D": {"Vendor":"Samsung", "Type":"Physical"}, "333300": {"Vendor":"IPv6", "Type":"Virtualization"}, "48F8B3": {"Vendor":"Linksys", "Type":"Physical"}, "5855CA": {"Vendor":"Apple", "Type":"Physical"}, "74C63B": {"Vendor":"AzureWave Technology", "Type":"Physical"}, "74D435": {"Vendor":"Giga-Byte Technology", "Type":"Physical"}, "FFFFFF": {"Vendor":"Broadcast", "Type":"Logical"} } # MAC passed as Python list, 6 elements def mac_parse( self, mac # type: list ): """ Parse MAC addresses passed as Python list(6) that has already been unpacked Args: mac (list): List with (6) elements Returns: tuple: (MAC Address:str, MAC OUI:str) """ mac_list = [] for mac_item in mac: mac_item_hex = hex(mac_item).replace('0x','') # Strip leading characters if len(mac_item_hex) == 1: mac_item_hex = str("0" + mac_item_hex) # Handle leading zeros and double-0's mac_list.append(mac_item_hex) parsed_mac = (':'.join(mac_list)).upper() # Format MAC as 00:11:22:33:44:AA parsed_mac_oui = (''.join(mac_list[0:3])).upper() # MAC OUI as 001122 return (parsed_mac,parsed_mac_oui) # MAC passed as packed bytes def mac_packed_parse( self, packed_data, # type: "XDR Data" pointer, # type: int field_size # type: int ): """ Parse MAC addresses passed as packed bytes that first need to be unpacked Args: packed_data (xdr): Packed XDR data pointer (int): Current location for unpacking field_size (int): Size of the packed data Returns: tuple: (MAC Address:str, MAC OUI:str) """ mac_list = [] mac_objects = self.struct.unpack('!%dB' % field_size,packed_data[pointer:pointer+field_size]) for mac_item in mac_objects: mac_item_hex = hex(mac_item).replace('0x','') # Strip leading characters if len(mac_item_hex) == 1: mac_item_hex = str("0" + mac_item_hex) # Handle leading zeros and double-0's mac_list.append(mac_item_hex) parsed_mac = (':'.join(mac_list)).upper() # Format MAC as 00:11:22:33:44:AA parsed_mac_oui = (''.join(mac_list[0:3])).upper() # MAC OUI as 001122 return (parsed_mac,parsed_mac_oui) # MAC OUI formatted "001122" def mac_oui( self, mac_oui_num # type: int ): """ Get MAC OUI (vendor,type) based on an OUI number formatted as '0011AA' Args: mac_oui_num (str): MAC OUI string eg 0011AA Returns: tuple: (Vendor, Type) """ try: return (self.mac_oui[mac_oui_num]["Vendor"],self.mac_oui[mac_oui_num]["Type"]) except NameError,KeyError: return False ### Generic ICMP Parsers ### class icmp_parse(object): def __init__(self): # ICMP Types and corresponding Codes self.icmp_table = { 0: {"Type":"Echo Reply","Codes": {0: "No Code"}}, 1: {"Type":"Unassigned"}, 2: {"Type":"Unassigned"}, 3: { "Type":"Destination Unreachable", "Codes": { 0: "Net Unreachable", 1: "Host Unreachable", 2: "Protocol Unreachable", 3: "Port Unreachable", 4: "Fragmentation Needed and Don't Fragment was Set", 5: "Source Route Failed", 6: "Destination Network Unknown", 7: "Destination Host Unknown", 8: "Source Host Isolated", 9: "Communication with Destination Network is Administratively Prohibited", 10: "Communication with Destination Host is Administratively Prohibited", 11: "Destination Network Unreachable for Type of Service", 12: "Destination Host Unreachable for Type of Service", 13: "Communication Administratively Prohibited ", 14: "Host Precedence Violation", 15: "Precedence cutoff in effect" } }, 4: {"Type":"Source Quench","Codes": {0: "No Code"}}, 5: { "Type":"Redirect", "Codes": { 0: "Redirect Datagram for the Network", 0: "Redirect Datagram for the Host", 0: "Redirect Datagram for the Type of Service and Network", 0: "Redirect Datagram for the Type of Service and Host" } }, 6: { "Type":"Alternate Host Address", "Codes": { 0: "Alternate Address for Host" } }, 7: {"Type":"Unassigned"}, 8: {"Type":"Echo","Codes": {0: "No Code"}}, 9: {"Type":"Router Advertisement","Codes": {0: "No Code"}}, 10: {"Type":"Router Selection","Codes": {0: "No Code"}}, 11: { "Type":"Time Exceeded", "Codes": { 0: "Time to Live exceeded in Transit", 1: "Fragment Reassembly Time Exceeded" } }, 12: { "Type":"Parameter Problem", "Codes": { 0: "Pointer indicates the error", 1: "Missing a Required Option", 2: "Bad Length" } }, 13: {"Type":"Timestamp","Codes": {0: "No Code"}}, 14: {"Type":"Timestamp Reply","Codes": {0: "No Code"}}, 15: {"Type":"Information Request","Codes": {0: "No Code"}}, 16: {"Type":"Information Reply","Codes": {0: "No Code"}}, 17: {"Type":"Address Mask Request","Codes": {0: "No Code"}}, 18: {"Type":"Address Mask Reply","Codes": {0: "No Code"}}, 19: {"Type":"Reserved"}, 20: {"Type":"Reserved"}, 21: {"Type":"Reserved"}, 22: {"Type":"Reserved"}, 23: {"Type":"Reserved"}, 24: {"Type":"Reserved"}, 25: {"Type":"Reserved"}, 26: {"Type":"Reserved"}, 27: {"Type":"Reserved"}, 28: {"Type":"Reserved"}, 29: {"Type":"Reserved"}, 30: {"Type":"Traceroute"}, 31: {"Type":"Datagram Conversion Error"}, 32: {"Type":"Mobile Host Redirect"}, 33: {"Type":"IPv6 Where-Are-You"}, 34: {"Type":"IPv6 I-Am-Here"}, 35: {"Type":"Mobile Registration Request"}, 36: {"Type":"Mobile Registration Reply"}, 37: {"Type":"Domain Name Request"}, 38: {"Type":"Domain Name Reply"}, 39: {"Type":"SKIP"}, 40: {"Type":"Photuris"} } # Parse human ICMP Type and Code from integers def icmp_human_type_code( self, icmp_reported # type: int ): """ Parse ICMP integer to get the human ICMP Type and Code Args: icmp_reported (int): Exported ICMP code [(256 * ICMP Type)+ICMP Code] Returns: tuple: (ICMP Type: str, ICMP Code: str) """ icmp_num_type = icmp_reported//256 # ICMP Type icmp_num_code = icmp_reported%256 # ICMP Code try: icmp_parsed_type = self.icmp_table[icmp_num_type]["Type"] try: icmp_parsed_code = self.icmp_table[icmp_num_type]["Codes"][icmp_num_code] except (NameError,KeyError): icmp_parsed_code = "No Code" return (icmp_parsed_type,icmp_parsed_code) # Return human ICMP Type and Code # Failed to parse ICMP Type / Code, just return original Type and Code numbers except (NameError,KeyError): return (icmp_num_type,icmp_num_code) def icmp_num_type_code( self, icmp_reported # type: int ): """ Parse ICMP integer to get the numeric ICMP Type and Code Args: icmp_reported (int): Exported ICMP code [(256 * ICMP Type)+ICMP Code] Returns: tuple: (ICMP Type: int, ICMP Code: int) """ icmp_num_type = icmp_reported//256 # ICMP Type icmp_num_code = icmp_reported%256 # ICMP Code return (icmp_num_type,icmp_num_code) ### Generic HTTP Parsers ### class http_parse(object): def __init__(self): return def http_code_category( self, http_code # type: int ): """ Reconcile an HTTP code to it's overall category eg 404 - Client Error Args: http_code (int): HTTP code eg 405 Returns: str: HTTP Code Category eg "Client Error" """ if http_code in range(100,200): return "Informational" elif http_code in range(200,300): return "Success" elif http_code in range(300,400): return "Redirection" elif http_code in range(400,500): return "Client Error" elif http_code in range(500,600): return "Server Error" else: return "Other" def http_code_parsed( self, http_code # type: int ): """ Parse HTTP codes to HTTP code names Args: http_code (int): HTTP code number eg "404" Returns: str: Parsed HTTP code eg "Not Found" """ if http_code == 200: return "OK" elif http_code == 201: return "Created" elif http_code == 202: return "Accepted" elif http_code == 100: return "Continue" elif http_code == 101: return "Switching Protocols" elif http_code == 102: return "Processing" elif http_code == 203: return "Non-Authoritative Information" elif http_code == 204: return "No Content" elif http_code == 205: return "Reset Content" elif http_code == 206: return "Partial Content" elif http_code == 207: return "Multi-Status" elif http_code == 208: return "Already Reported" elif http_code == 226: return "IM Used" elif http_code == 300: return "Multiple Choices" elif http_code == 301: return "Moved Permanently" elif http_code == 302: return "Found" elif http_code == 303: return "See Other" elif http_code == 304: return "Not Modified" elif http_code == 305: return "Use Proxy" elif http_code == 306: return "Switch Proxy" elif http_code == 307: return "Temporary Redirect" elif http_code == 308: return "Permanent Redirect" elif http_code == 400: return "Bad Request" elif http_code == 401: return "Unauthorized" elif http_code == 402: return "Payment Required" elif http_code == 403: return "Forbidden" elif http_code == 404: return "Not Found" elif http_code == 405: return "Method Not Allowed" elif http_code == 406: return "Not Acceptable" elif http_code == 407: return "Proxy Authentication Required" elif http_code == 408: return "Request Time-Out" elif http_code == 409: return "Conflict" elif http_code == 410: return "Gone" elif http_code == 411: return "Length Required" elif http_code == 412: return "Precondition Failed" elif http_code == 413: return "Payload Too Large" elif http_code == 414: return "URI Too Long" elif http_code == 415: return "Unsupported Media Type" elif http_code == 416: return "Range Not Satisfiable" elif http_code == 417: return "Expectation Failed" elif http_code == 418: return "Teapot" elif http_code == 421: return "Misdirected Request" elif http_code == 422: return "Unprocessable Entity" elif http_code == 423: return "Locked" elif http_code == 424: return "Failed Dependency" elif http_code == 426: return "Upgrade Required" elif http_code == 428: return "Precondition Required" elif http_code == 429: return "Too Many Requests" elif http_code == 431: return "Request Header Fields Too Large" elif http_code == 451: return "Unavailable For Legal Reasons" elif http_code == 500: return "Internal Server Error" elif http_code == 501: return "Not Implemented" elif http_code == 502: return "Bad Gateway" elif http_code == 503: return "Service Unavailable" elif http_code == 504: return "Gateway Time-Out" elif http_code == 505: return "HTTP Version Not Supported" elif http_code == 506: return "Variant Also Negotiates" elif http_code == 507: return "Insufficient Storage" elif http_code == 508: return "Loop Detected" elif http_code == 510: return "Not Extended" elif http_code == 511: return "Network Authentication Required" else: return "Other" ### Netflow v9 Parsers ### class netflowv9_parse(object): from struct import unpack from collections import OrderedDict from field_types import v9_fields def __init__(self): return # Parsing template flowset def template_flowset_parse( self, packed_data, # type: struct sensor, # type: str pointer, # type: int length # type: int ): """ Unpack a Netflow v9 template Args: packed_data (struct): Packed data sensor (str): Netflow v9 sensor pointer (int): Current unpack location length (int): Length of data to unpack Returns: dict[Hashed ID]: {Sensor: str, Template ID: int, Length: int, Type: int, Definitions: dict} """ cache = {} while pointer < length: (template_id, template_field_count) = self.unpack('!HH',packed_data[pointer:pointer+4]) pointer += 4 # Advance the field hashed_id = hash(str(sensor)+str(template_id)) cache[hashed_id] = {} cache[hashed_id]["Sensor"] = str(sensor) cache[hashed_id]["Template ID"] = template_id cache[hashed_id]["Length"] = template_field_count # Field count cache[hashed_id]["Type"] = "Flow Data" cache[hashed_id]["Definitions"] = self.OrderedDict() for _ in range(0,template_field_count): # Iterate through each line in the template (element, element_length) = self.unpack('!HH',packed_data[pointer:pointer+4]) if element in self.v9_fields: # Fields we know about and support cache[hashed_id]["Definitions"][element] = element_length pointer += 4 # Advance the field return cache # Parsing option template def option_template_parse( self, packed_data, # type: struct sensor, # type: str pointer # type: int ): """ Unpack a Netflow v9 option template Args: packed_data (struct): Packed data sensor (str): Netflow v9 sensor pointer (int): Current unpack location Returns: dict[Hashed ID]: {Sensor: str, Template ID: int, Type: str, Scope Fields: dict, Option Fields: dict} """ (option_template_id,option_scope_length,option_length) = self.unpack('!HHH',packed_data[pointer:pointer+6]) pointer += 6 # Move ahead 6 bytes cache = {} hashed_id = hash(str(sensor)+str(option_template_id)) # Hash for individual sensor and template ID cache[hashed_id] = {} cache[hashed_id]["Sensor"] = str(sensor) cache[hashed_id]["Template ID"] = option_template_id cache[hashed_id]["Type"] = "Options Template" cache[hashed_id]["Scope Fields"] = self.OrderedDict() cache[hashed_id]["Option Fields"] = self.OrderedDict() for x in range(pointer,pointer+option_scope_length,4): (scope_field_type,scope_field_length) = self.unpack('!HH',packed_data[x:x+4]) cache[hashed_id]["Scope Fields"][scope_field_type] = scope_field_length pointer += option_scope_length for x in range(pointer,pointer+option_length,4): (option_field_type,option_field_length) = self.unpack('!HH',packed_data[x:x+4]) cache[hashed_id]["Option Fields"][option_field_type] = option_field_length pointer += option_length return cache ### Protocol and Port Parsers ### class ports_and_protocols(object): # Field types, defined ports, etc from defined_ports import registered_ports,other_ports from protocol_numbers import protocol_type def __init__(self): return # Tag "Traffic Category" by Protocol classification ("Routing", "ICMP", etc.) def protocol_traffic_category( self, protocol_number # type: int ): """ Reconcile protocol numbers to a Category eg 89 to "Routing" Args: protocol_number (int): Traffic type eg 89 to "Routing" Returns: str: Traffic Category eg "Routing" """ try: return self.protocol_type[protocol_number]["Category"] except (NameError,KeyError): return "Other" # Tag traffic by SRC and DST port def port_traffic_classifier( self, src_port, # type: int dst_port # type: int ): """ Reconcile port numbers to services eg TCP/80 to HTTP, and services to categories eg HTTP to Web Args: src_port (int): Port number eg "443" dst_port (int): Port number eg "443" Returns: dict: ["Traffic":"HTTP","Traffic Category":"Web"], default value is "Other" for each. """ traffic = {} # SRC Port if src_port in self.registered_ports: traffic["Traffic"] = self.registered_ports[src_port]["Name"] if "Category" in self.registered_ports[src_port]: traffic["Traffic Category"] = self.registered_ports[src_port]["Category"] elif src_port in self.other_ports: traffic["Traffic"] = self.other_ports[src_port]["Name"] if "Category" in self.other_ports[src_port]: traffic["Traffic Category"] = self.other_ports[src_port]["Category"] else: pass # DST Port if dst_port in self.registered_ports: traffic["Traffic"] = self.registered_ports[dst_port]["Name"] if "Category" in self.registered_ports[dst_port]: traffic["Traffic Category"] = self.registered_ports[dst_port]["Category"] elif dst_port in self.other_ports: traffic["Traffic"] = self.other_ports[dst_port]["Name"] if "Category" in self.other_ports[dst_port]: traffic["Traffic Category"] = self.other_ports[dst_port]["Category"] else: pass try: # Set as "Other" if not already set traffic["Traffic"] except (NameError,KeyError): traffic["Traffic"] = "Other" try: # Set as "Other" if not already set traffic["Traffic Category"] except (NameError,KeyError): traffic["Traffic Category"] = "Other" return traffic