netscan-ks 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: netscan-ks
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.8
5
+ Requires-Dist: scapy
6
+ Requires-Dist: tqdm
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: netscan-ks
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.8
5
+ Requires-Dist: scapy
6
+ Requires-Dist: tqdm
@@ -0,0 +1,11 @@
1
+ pyproject.toml
2
+ netscan_ks.egg-info/PKG-INFO
3
+ netscan_ks.egg-info/SOURCES.txt
4
+ netscan_ks.egg-info/dependency_links.txt
5
+ netscan_ks.egg-info/entry_points.txt
6
+ netscan_ks.egg-info/requires.txt
7
+ netscan_ks.egg-info/top_level.txt
8
+ scanner/__init__.py
9
+ scanner/banner_grab.py
10
+ scanner/cli.py
11
+ scanner/syn_scan.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ netscan = scanner.cli:run
@@ -0,0 +1,2 @@
1
+ scapy
2
+ tqdm
@@ -0,0 +1 @@
1
+ scanner
@@ -0,0 +1,12 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "netscan-ks"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.8"
9
+ dependencies = ["scapy", "tqdm"]
10
+
11
+ [project.scripts]
12
+ netscan = "scanner.cli:run"
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,12 @@
1
+ import socket
2
+
3
+ def grab_banner(target, port, timeout=2):
4
+ try:
5
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
6
+ sock.settimeout(timeout)
7
+ sock.connect((target, port))
8
+ banner = sock.recv(1024).decode().strip()
9
+ sock.close()
10
+ return banner
11
+ except(socket.timeout, socket.error, UnicodeDecodeError):
12
+ return None
@@ -0,0 +1,103 @@
1
+ import argparse
2
+ from math import e
3
+ import threading
4
+ import time
5
+ import ipaddress
6
+ from tqdm import tqdm
7
+ from scanner.syn_scan import syn_scan
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from scanner.banner_grab import grab_banner
10
+
11
+
12
+ def run():
13
+ threading.excepthook = scapy_thread_error_filter
14
+ start_time = time.time()
15
+
16
+ parser = argparse.ArgumentParser()
17
+ parser.add_argument("-t", "--target", help="Target IP address to scan", required=True)
18
+ parser.add_argument("-p", "--ports", help="Specific ports to scan", nargs="+")
19
+ parser.add_argument("-s", "--start", help="Start of port range to scan" ,type=int)
20
+ parser.add_argument("-e", "--end", help="End of port range to scan", type=int)
21
+ args = parser.parse_args()
22
+
23
+ target = args.target
24
+ targeted_port = args.ports
25
+ start_port = args.start
26
+ end_port = args.end
27
+ open_ports = []
28
+ filtered_ports = []
29
+
30
+ print(f"Starting scan on {target}...")
31
+
32
+ #Error checks
33
+ if bool(start_port) != bool(end_port):
34
+ print("Error: --start and --end must be used together.")
35
+ return
36
+
37
+ try:
38
+ ipaddress.ip_address(target)
39
+ except ValueError:
40
+ print(f"Error: '{target}' is not a valid IP address.")
41
+ return
42
+
43
+ if start_port and end_port:
44
+ if not (1 <= start_port <= 65535) or not (1 <= end_port <= 65535):
45
+ print("Error: Ports must be between 1 and 65535.")
46
+ return
47
+ if start_port > end_port:
48
+ print("Error: Start port must be less than or equal to end port.")
49
+ return
50
+
51
+ #Scanning starts here
52
+ if targeted_port:
53
+ ports = [int(p.strip(",")) for p in targeted_port]
54
+ if any(not (1 <= p <= 65535) for p in ports):
55
+ print("Error: Ports must be between 1 and 65535.")
56
+ return
57
+ with ThreadPoolExecutor(max_workers=50) as executor:
58
+ results = list(tqdm(executor.map(lambda port: scan_and_store(target, port), ports),
59
+ total=len(ports), desc="Scanning"))
60
+ for port, result in results:
61
+ if result == "OPEN":
62
+ open_ports.append(port)
63
+ elif "FILTERED" in result:
64
+ filtered_ports.append(port)
65
+
66
+ if start_port and end_port:
67
+ with ThreadPoolExecutor(max_workers=50) as executor:
68
+ results = list(tqdm(executor.map(lambda port: scan_and_store(target, port), range(start_port, end_port + 1)),
69
+ total=end_port - start_port + 1, desc="Scanning"))
70
+ for port, result in results:
71
+ if result == "OPEN":
72
+ open_ports.append(port)
73
+ elif "FILTERED" in result:
74
+ filtered_ports.append(port)
75
+
76
+ #Outputs
77
+ print(f"Open ports:{len(open_ports)}")
78
+ for port in open_ports:
79
+ banner = grab_banner(target, port)
80
+ if banner:
81
+ print(f"Port {port} is OPEN - Banner: {banner}")
82
+ else:
83
+ print(f"Port {port} is OPEN")
84
+
85
+ print(f"Filtered ports:{len(filtered_ports)}")
86
+ for port in filtered_ports:
87
+ print(f"Port {port} is FILTERED")
88
+
89
+ elapsed = time.time() - start_time
90
+ print(f"Scan completed in {elapsed:.2f} seconds.")
91
+
92
+ def scan_and_store(target, port):
93
+ result = syn_scan(target, port)
94
+ return (port, result)
95
+
96
+ # Scapy's sr1() spawns internal threads for packet sending/receiving.
97
+ # When running my own scans with ThreadPoolExecutor, these internal threads occasionally fail with OSError errno 9 (Bad file descriptor)
98
+ # and errno 22 (Invalid argument) during cleanup. These are known Scapy pipe errors on Windows and do not affect scan results.
99
+ # threading.excepthook is used to suppress these specific errors only. To clarify, the errors do occur on every scan, they are just not visible to the user.
100
+ def scapy_thread_error_filter(args):
101
+ if isinstance(args.exc_value, OSError) and args.exc_value.errno in (9, 22):
102
+ return
103
+ threading.__excepthook__(args)
@@ -0,0 +1,34 @@
1
+ from scapy.layers.inet import IP, TCP, ICMP
2
+ from scapy.sendrecv import sr1
3
+ import random
4
+
5
+ def syn_scan(target, port, retries=2, timeout=2):
6
+ for attempt in range(retries + 1):
7
+ sport = random.randint(1024, 65535)
8
+ packet = IP(dst=target) / TCP(sport=sport,
9
+ dport=port,
10
+ flags="S")
11
+
12
+ response = sr1(packet, timeout=timeout, verbose=0)
13
+
14
+ if response is None:
15
+ if attempt == retries:
16
+ return "FILTERED"
17
+ continue
18
+
19
+ if response.haslayer(TCP):
20
+ flags = response.getlayer(TCP).flags
21
+ if flags & 0x12 == 0x12: # SYN-ACK
22
+ rst = IP(dst=target) / TCP(sport=sport,
23
+ dport=port,
24
+ flags="R",
25
+ seq=response.ack)
26
+ sr1(rst, timeout=timeout, verbose=0)
27
+ return "OPEN"
28
+ elif flags & 0x14 == 0x14: # RST-ACK
29
+ return "CLOSED"
30
+ elif response.haslayer(ICMP):
31
+ icmp = response.getlayer(ICMP)
32
+ if icmp.type == 3:
33
+ return f"FILTERED (ICMP type 3 code {icmp.code})"
34
+ return "UNKNOWN"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+