Source code for tech_tools.cli

import platform
import subprocess
from ipaddress import IPv4Address, AddressValueError
import threading

import pandas as pd


# IPCONFIG
[docs] def ipconfig(): """Return string of raw ip configuration info from CLI. :return: Printout of ipconifg /all on Windows or nmcli device show on linux :rtype: str Note: Use of this function on Linux requires that nmcli be installed. """ operating_system = platform.system().lower() # Defaults to windows command command = ["ipconfig", "/all"] if operating_system == "linux": command = ["nmcli", "device", "show"] raw_ipconfig = subprocess.run(command, capture_output=True, text=True) ipconfig_output = raw_ipconfig.stdout return ipconfig_output
[docs] def parse_ipconfig(): """Parse raw ipconfig information and return a list containing a dictionary for each valid interface. :return: List of dictionaries with keys: ip, subnet, mac, (gateway if present) :rtype: list Note: Subnet will be provided as a mask on Windows (ex 255.255.255.0) and using CIDR notation on Linux (ex /24). Valid interfaces might not have a defined gateway. """ operating_system = platform.system().lower() raw_output = ipconfig() found_interfaces = [] # Split by blank lines interfaces = raw_output.split("\n\n") if operating_system == "linux": items_of_interest = { "GENERAL.HWADDR": "mac", "IP4.ADDRESS[1]": "cidr_notation", "IP4.GATEWAY": "gateway", } for interface in interfaces: interface_dict = {} lines = interface.splitlines() # Replace ': ' with an asterisk as it makes splitting easier in following step, remove whitespace lines = [line.replace(": ", "*").replace(" ", "") for line in lines] # Create two pieces lines = [line.split("*") for line in lines] for line in lines: # If first piece is a key in items_of_interest if line[0] in items_of_interest.keys(): # AND second piece isn't a blank value if line[1] != "--": # Use its accompanying value in items_of_interest as its new key, second piece is its value interface_dict[items_of_interest[line[0]]] = line[1] found_interfaces.append(interface_dict) # Only interfaces with a valid cidr_notation value are desired found_interfaces = [ interface for interface in found_interfaces if "cidr_notation" in interface.keys() ] for interface in found_interfaces: # Split cidr_notation value into an ip address and a subnet value, remove original cidr_notation pair cidr_notation = interface["cidr_notation"] splits = cidr_notation.split("/") interface["ip"] = splits[0] interface["subnet"] = splits[1] del interface["cidr_notation"] else: items_of_interest = { "Physical Address": "mac", "IPv4 Address": "ip", "Subnet Mask": "subnet", "Default Gateway": "gateway", } # Keep lines with IPv4 Address included valid_interfaces = [ interface for interface in interfaces if "IPv4" in interface ] for interface in valid_interfaces: interface_dict = {} lines = interface.splitlines() # Remove white space to simplify upcoming sections stripped_lines = [item.strip() for item in lines] for line in stripped_lines: # Create two pieces to work with line = line.split(":") # Replace various undesirable text using multiple steps new_line = [line[0].replace(".", "").strip(), line[1].strip()] new_line = [ item.replace("(Preferred)", "").replace("-", ":") for item in new_line ] # Check if the first piece is a key in items_of_interest if new_line[0] in items_of_interest.keys(): # Use its accompanying value in items_of_interest as its new key, second piece is its value interface_dict[items_of_interest[new_line[0]]] = new_line[1] found_interfaces.append(interface_dict) return found_interfaces
# ARP
[docs] def local_arp(): """Return string of raw ip configuration info from CLI. :return: Printout of the local arp table :rtype: str """ operating_system = platform.system().lower() # Defaults to windows command command = ["arp", "-a"] if operating_system == "linux": command = ["arp", "-n"] raw_arp = subprocess.run(command, capture_output=True, text=True) arp_output = raw_arp.stdout return arp_output
[docs] def parse_local_arp(): """Parse raw local arp data and return a Pandas DataFrame. :return: Parsed information from the local arp table, IPv4 Address and Mac Address :rtype: Pandas.DataFrame columns: ip, mac """ operating_system = platform.system().lower() raw_arp_string = local_arp() arp_info = raw_arp_string.splitlines() # Remove line(s) that contain header information arp_info = [ line.split() for line in arp_info if "Address" not in line if "Interface" not in line if len(line) > 1 ] # On windows, the first two items in each list are ip, mac selected_items = [[item[0], item[1]] for item in arp_info] if operating_system == "linux": # On linux, the first and third items are ip, mac selected_items = [[item[0], item[2]] for item in arp_info] arp_df = pd.DataFrame(selected_items, columns=["ip", "mac"]) # Convert MAC information to upper case for later comparison to manufacture database. arp_df["mac"] = arp_df["mac"].str.upper() # Replace - with : arp_df["mac"] = arp_df["mac"].str.replace("-", ":") # Covert strings to IPv4 Address objects arp_df["ip"] = arp_df["ip"].apply(lambda row: IPv4Address(row)) return arp_df
# Ping
[docs] def ping_single_ip(ip, output): """Ping a single host and append to output list if successful. :param ip: A valid IPv4 Address, example "10.10.10.132" :type ip: str, IPv4Address :param output: List to update with values :type output: list :return: Nothing, external list will be updated :rtype: None """ operating_system = platform.system().lower() # Convert to string if not already ip = str(ip) # Default to windows command command = ["ping", ip] # Note the linux ping command requires additional flag to prevent an unending process if operating_system == "linux": command = ["ping", ip, "-c", "4"] ping = subprocess.run(command, capture_output=True, text=True) # Note that windows has TTL uppercase if "ttl" in ping.stdout.lower(): output.append(IPv4Address(ip))
[docs] def ping_range_ip(ip_list): """Ping a list of hosts and return list of hosts that produced a valid response. :param ip_list: Containing either str or IPv4Address objects of hosts :type ip_list: list :return: IPv4Address objects that responded to a ping :rtype: list """ output = [] threads_list = [] # Create separate thread for each host to expedite the process # Most of the time in this function is consumed by waiting for a host response for ip in ip_list: t = threading.Thread(target=ping_single_ip, args=(ip, output)) threads_list.append(t) for number in range(len(threads_list)): threads_list[number].start() for number in range(len(threads_list)): threads_list[number].join() # Sort the output to keep addresses in a user-friendly order return sorted(output)
# Trace Route
[docs] def trace_route(destination="8.8.8.8"): """Determine route from local host to a given destination and return raw string data. :param destination: (optional) Remote host, 8.8.8.8 by default :type destination: str, IPv4Address :return: Printout of tracert on Windows or traceroute on Linux Note: Use of this function on Linux requires that traceroute be installed. """ operating_system = platform.system().lower() # Convert destination to str if not already destination = str(destination) # Do not resolve hostnames, 100ms timeout to improve speed slightly command = ["tracert", "-d", "-w", "100", destination] if operating_system == "linux": # Do not resolve hostnames, IP information is desirable command = ["traceroute", destination, "-n"] raw_trace = subprocess.run(command, capture_output=True, text=True) trace_output = raw_trace.stdout return trace_output
[docs] def parse_trace_route_local(destination="8.8.8.8"): """Parse raw trace route data into a list of hosts considered to be part of local/private networks. :param destination: (optional) Remote host, 8.8.8.8 by default :type destination: str, IPv4Address :return: Hosts (hops) along a given trace route with local (private) addresses :rtype: list """ operating_system = platform.system().lower() raw_trace_string = trace_route(destination) # Split by lines to dissect the information trace_info = raw_trace_string.splitlines() if operating_system == "linux": # Remove line(s) that contain header information trace_info = [ line.split() for line in trace_info if "hops" not in line if "route" not in line ] # On linux, the second item contains the IP address selected_items = [item[1] for item in trace_info] else: # Remove unwanted lines beginning and end lines have 'Trac', blank lines will be length of one trace_info = [ line.split() for line in trace_info if "Trac" not in line if len(line) > 1 ] # On windows, the final item will be the IP address selected_items = [item[-1] for item in trace_info] private_ips = [] for ip in selected_items: try: ip_object = IPv4Address(ip) # Only local IP addresses are of interest if ip_object.is_private: private_ips.append(ip_object) # Some hosts will return * or similar instead of valid response, except these except AddressValueError: pass return sorted(private_ips)