diff --git a/scan.py b/scan.py index 44a9a48..44cccde 100644 --- a/scan.py +++ b/scan.py @@ -1,4 +1,3 @@ - import ipaddress import subprocess import argparse @@ -6,53 +5,96 @@ import csv import json import time import threading -import itertools +import sys from concurrent.futures import ThreadPoolExecutor, as_completed default_oids = { - "sysName": "1.3.6.1.2.1.1.5.0", - #"sysDescr": "1.3.6.1.2.1.1.1.0", - #"sysLocation": "1.3.6.1.2.1.1.6.0", - #"sysContact": "1.3.6.1.2.1.1.4.0" + "sysName": "1.3.6.1.2.1.1.5.0" } -def get_oid_value(ip, community, oid, timeout=1): - try: - result = subprocess.run( - ["snmpget", "-v2c", "-c", community, str(ip), oid], - capture_output=True, text=True, timeout=timeout - ) - if result.returncode == 0 and "STRING" in result.stdout: - return result.stdout.split("STRING:")[-1].strip() - except subprocess.TimeoutExpired: - return "Timeout" - return "Brak danych" +class Scanner: + def __init__(self): + self.stop_event = threading.Event() + self.lock = threading.Lock() + self.counter = [0] + self.results = [] + self.last_ip = None + self.errors = [0] -def query_all_oids(ip, community, oids, timeout=1): - values = {name: get_oid_value(ip, community, oid, timeout) for name, oid in oids.items()} - has_data = any(val not in ("Timeout", "Brak danych") for val in values.values()) - if has_data: - return (str(ip), values) - return None + def get_oid_value(self, ip, community, oid, timeout=1): + if self.stop_event.is_set(): + return "Przerwano" + try: + result = subprocess.run( + ["snmpget", "-v2c", "-c", community, str(ip), oid], + capture_output=True, text=True, timeout=timeout + ) + if result.returncode == 0 and "STRING" in result.stdout: + return result.stdout.split("STRING:")[-1].strip() + else: + with self.lock: + self.errors[0] += 1 + except subprocess.TimeoutExpired: + with self.lock: + self.errors[0] += 1 + return "Timeout" + except Exception: + with self.lock: + self.errors[0] += 1 + return "Błąd" + return "Brak danych" -def scan_subnet(subnet, community, oids, counter, total, lock): - results = [] - for ip in subnet.hosts(): - result = query_all_oids(ip, community, oids, timeout=1) - if result: - results.append(result) - with lock: - counter[0] += 1 - print(f"\rSkanowanie IP: {counter[0]}/{total}", end="", flush=True) - return results + def query_all_oids(self, ip, community, oids, timeout=1): + if self.stop_event.is_set(): + return None + values = {name: self.get_oid_value(ip, community, oid, timeout) for name, oid in oids.items()} + has_data = any(val not in ("Timeout", "Brak danych", "Błąd", "Przerwano") for val in values.values()) + if has_data: + return (str(ip), values) + return None -def split_subnet(supernet, new_prefix=24): - try: - network = ipaddress.IPv4Network(supernet) - return list(network.subnets(new_prefix=new_prefix)) - except ValueError as e: - print(f"Błąd podsieci: {e}") - return [] + def scan_subnet(self, subnet, community, oids, oids_order, live_mode): + for ip in subnet.hosts(): + if self.stop_event.is_set(): + return + result = self.query_all_oids(ip, community, oids) + with self.lock: + self.last_ip = str(ip) + self.counter[0] += 1 + if result: + self.results.append(result) + if live_mode: + ip_str, values = result + row = f"{ip_str:<15} " + " ".join([f"{values.get(k, ''):<25}" for k in oids_order]) + sys.stdout.write("\r" + " " * 120 + "\r") + sys.stdout.write(row + "\n") + sys.stdout.flush() + + def show_progress(self, total): + start_time = time.time() + while not self.stop_event.is_set(): + with self.lock: + done = self.counter[0] + last_ip = self.last_ip or "-" + errors = self.errors[0] + + elapsed = time.time() - start_time + speed = done / elapsed if elapsed > 0 else 0 + avg_time_per_host = elapsed / done if done else 0 + eta = (total - done) / speed if speed > 0 else float("inf") + eta_str = time.strftime("%H:%M:%S", time.gmtime(eta)) if eta != float("inf") else "N/A" + + sys.stdout.write( + f"\rPostęp: {done}/{total} ({(done/total)*100:.2f}%)" + f" | {speed:.2f} hostów/s" + f" | Śr. czas/hosta: {avg_time_per_host:.3f} s" + f" | Błędy: {errors}" + f" | Ostatni IP: {last_ip}" + f" | ETA: {eta_str}" + ) + sys.stdout.flush() + time.sleep(0.1) + sys.stdout.write("\n") def read_subnets_from_file(filename): subnets = [] @@ -63,9 +105,17 @@ def read_subnets_from_file(filename): if line: subnets.append(line) except Exception as e: - print(f"Błąd podczas wczytywania pliku podsieci: {e}") + print(f"Błąd podczas wczytywania pliku: {e}") return subnets +def split_subnet(supernet, new_prefix=24): + try: + network = ipaddress.IPv4Network(supernet) + return list(network.subnets(new_prefix=new_prefix)) + except ValueError as e: + print(f"Błąd podsieci: {e}") + return [] + def save_to_csv(results, oids, filename="wyniki.csv"): with open(filename, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) @@ -75,55 +125,87 @@ def save_to_csv(results, oids, filename="wyniki.csv"): row = [ip] + [values.get(key, "") for key in oids.keys()] writer.writerow(row) +def sort_results(results, key): + return sorted(results, key=lambda x: x[1].get(key, "").lower()) + def main(): - parser = argparse.ArgumentParser(description="Skaner SNMP z podziałem podsieci i eksportem do CSV.") - parser.add_argument("subnets", nargs="*", help="Podsieci w formacie CIDR (np. 172.16.0.0/16)") - parser.add_argument("-s", "--subnet-file", help="Plik tekstowy z listą podsieci CIDR (jedna na linię)") + parser = argparse.ArgumentParser(description="SNMP skaner https://gitea.linuxiarz.pl/gru/snmp_skaner") + parser.add_argument("subnets", nargs="*", help="Podsieci w formacie CIDR (np. 192.168.1.0/24)") + parser.add_argument("-s", "--subnet-file", help="Plik z listą podsieci (jedna na linię)") parser.add_argument("-c", "--community", default="public", help="SNMP community (domyślnie: public)") - parser.add_argument("-o", "--oids", help="OID-y w formacie JSON, np. '{\"sysDescr\":\"1.3.6.1.2.1.1.1.0\"}'") - parser.add_argument("-w", "--workers", type=int, default=10, help="Liczba równoległych wątków (domyślnie: 10)") - parser.add_argument("-p", "--prefix", type=int, default=24, help="Wielkość podsieci do podziału (domyślnie: 24)") - parser.add_argument("-f", "--file", default="wyniki.csv", help="Nazwa pliku CSV do zapisu (domyślnie: wyniki.csv)") + parser.add_argument("-o", "--oids", help="OID-y w formacie JSON, np. '{\"sysName\":\"...\"}'") + parser.add_argument("-w", "--workers", type=int, default=10, help="Liczba wątków (domyślnie: 10)") + parser.add_argument("-p", "--prefix", type=int, default=24, help="Prefix dla podsieci (domyślnie: 24)") + parser.add_argument("-f", "--file", default="wyniki.csv", help="Plik CSV wyników") + parser.add_argument("--live", action="store_true", help="Tryb wypisywania wyników na żywo") + parser.add_argument("--sort", help="Pole OID do sortowania wyników (np. sysName)") args = parser.parse_args() - oids = json.loads(args.oids) if args.oids else default_oids + scanner = Scanner() + + try: + oids = json.loads(args.oids) if args.oids else default_oids + oids_order = list(oids.keys()) - all_input_subnets = [] - if args.subnet_file: - all_input_subnets.extend(read_subnets_from_file(args.subnet_file)) - if args.subnets: - all_input_subnets.extend(args.subnets) + all_subnets = [] + if args.subnet_file: + all_subnets += read_subnets_from_file(args.subnet_file) + if args.subnets: + all_subnets += args.subnets + if not all_subnets: + print("Brak podsieci do skanowania.") + return - if not all_input_subnets: - print("Brak podsieci do przeskanowania (ani z linii poleceń, ani z pliku).") - return + subnets_to_scan = [] + for subnet in all_subnets: + subnets_to_scan.extend(split_subnet(subnet, args.prefix)) - subnets_to_scan = [] - for subnet in all_input_subnets: - subnets_to_scan.extend(split_subnet(subnet, new_prefix=args.prefix)) + total_ips = sum(1 for subnet in subnets_to_scan for _ in subnet.hosts()) - # Oblicz łączną liczbę hostów - total_ips = sum(1 for subnet in subnets_to_scan for _ in subnet.hosts()) - counter = [0] - lock = threading.Lock() + if args.live: + print(f"\n{'IP':<15} " + " ".join([f"{name:<25}" for name in oids_order])) + print("-" * (15 + 26 * len(oids_order))) - print(f"{'IP':<15} " + " ".join([f"{name:<25}" for name in oids.keys()])) - print("-" * (15 + 26 * len(oids))) + progress_thread = threading.Thread(target=scanner.show_progress, args=(total_ips,)) + progress_thread.daemon = True + progress_thread.start() - all_results = [] - with ThreadPoolExecutor(max_workers=args.workers) as executor: - futures = { - executor.submit(scan_subnet, subnet, args.community, oids, counter, total_ips, lock): subnet - for subnet in subnets_to_scan - } - for future in as_completed(futures): - subnet_results = future.result() - for ip_str, values in subnet_results: - all_results.append((ip_str, values)) - print(f"\n{ip_str:<15} " + " ".join([f"{values[name]:<25}" for name in oids.keys()])) + with ThreadPoolExecutor(max_workers=args.workers) as executor: + futures = [] + for subnet in subnets_to_scan: + if scanner.stop_event.is_set(): + break + futures.append(executor.submit( + scanner.scan_subnet, subnet, args.community, oids, oids_order, args.live + )) + + try: + for future in as_completed(futures): + future.result() + except KeyboardInterrupt: + scanner.stop_event.set() + print("\nPrzerwano przez użytkownika. Zapisuję dotychczasowe wyniki...") - print("\nSkanowanie zakończone.") - save_to_csv(all_results, oids, filename=args.file) + if not args.live: + if args.sort and args.sort in oids: + scanner.results = sort_results(scanner.results, args.sort) + + print(f"\n{'IP':<15} " + " ".join([f"{name:<25}" for name in oids_order])) + print("-" * (15 + 26 * len(oids_order))) + for ip, values in scanner.results: + print(f"{ip:<15} " + " ".join([f"{values.get(k, ''):<25}" for k in oids_order])) + + save_to_csv(scanner.results, oids, filename=args.file) + print(f"\nZapisano do pliku: {args.file}") + + except KeyboardInterrupt: + scanner.stop_event.set() + print("\nPrzerwano przez użytkownika. Zapisuję dotychczasowe wyniki...") + save_to_csv(scanner.results, oids, filename=args.file) + except Exception as e: + scanner.stop_event.set() + print(f"\nWystąpił błąd: {str(e)}") + save_to_csv(scanner.results, oids, filename=args.file) if __name__ == "__main__": - main() \ No newline at end of file + main()