Project Overview
netscan.py is a lightweight, dependency-free Python network scanning tool that discovers live hosts across a network and identifies their open TCP ports. Results are automatically exported to a timestamped CSV file, giving you a clean, shareable record of your network's attack surface.
Why I Built This
Visibility is the Foundation of Security
You can't protect what you can't see. In my role managing servers and endpoints at the University of Utah, one of the first steps in any security review is understanding what's actually exposed on the network. Commercial tools exist, but they're often overkill, require licensing, or can't be quickly deployed in a new environment.
Automation Over Manual Effort
Manually checking ports across dozens of servers is error-prone and time-consuming. This tool automates the entire process — scan, resolve, and report — in a single command. The CSV output integrates directly into spreadsheets, ticketing systems like ServiceNow, or vulnerability management workflows.
Practical Cybersecurity Skill Development
Building this tool from scratch — using only Python's standard library — deepened my understanding of TCP socket programming, concurrency, and network fundamentals. It's the kind of hands-on project that bridges the gap between theory and real-world security operations.
How It Works
1. Target Expansion
Accepts individual IPs, hostnames, or CIDR notation (e.g., 192.168.1.0/24). CIDR ranges are expanded into individual host addresses using Python's ipaddress module.
2. Concurrent Port Scanning
A ThreadPoolExecutor runs up to 50 threads simultaneously, each attempting a TCP connect_ex() with a 1-second timeout. This makes scanning a /24 subnet fast without flooding the network.
3. Hostname Resolution
For each live host, a reverse DNS lookup is attempted via socket.gethostbyaddr(). If no PTR record exists, the hostname field is left blank — no errors, no crashes.
4. CSV Export
Only hosts with at least one open port are written to the output. The CSV contains three columns: IP address, hostname, and a comma-separated list of open ports.
Default Ports Scanned
These ports represent the most commonly exploited services in enterprise environments — from legacy protocols like Telnet and FTP to modern database and remote access services.
Usage
Scan a single host
python3 netscan.py 192.168.1.1
Scan an entire subnet
python3 netscan.py 192.168.1.0/24
Scan specific ports across multiple targets
python3 netscan.py 10.0.0.0/24 example.com -p 22,80,443,3389
Custom output file and thread count
python3 netscan.py 192.168.1.0/24 -o results.csv -w 100
CSV Output Format
| ip | hostname | open_ports |
|---|---|---|
| 192.168.1.1 | router.local | 22,80,443 |
| 192.168.1.45 | workstation-05.local | 3389 |
| 192.168.1.100 | 22,3306,8080 |
Full Source Code
The complete script is ~90 lines of pure Python with no external dependencies. Download it from GitHub or copy it directly below.
#!/usr/bin/env python3
"""Network scanner: discovers hosts and open ports, outputs CSV."""
import csv
import socket
import sys
import ipaddress
import concurrent.futures
from datetime import datetime
DEFAULT_PORTS = [21, 22, 23, 25, 53, 80, 110, 143, 443, 445,
3306, 3389, 5432, 6379, 8080, 8443, 27017]
def resolve_hostname(host):
try:
return socket.gethostbyaddr(host)[0]
except socket.herror:
return ""
def scan_port(host, port, timeout=1.0):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
return s.connect_ex((host, port)) == 0
def scan_host(host, ports):
open_ports = [p for p in ports if scan_port(host, p)]
if not open_ports:
return None
return {
"ip": host,
"hostname": resolve_hostname(host),
"open_ports": ",".join(map(str, open_ports)),
}
def expand_targets(targets):
hosts = []
for t in targets:
try:
net = ipaddress.ip_network(t, strict=False)
hosts.extend(str(ip) for ip in net.hosts())
except ValueError:
hosts.append(t)
return hosts
def scan(targets, ports, workers=50):
hosts = expand_targets(targets)
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(scan_host, h, ports): h for h in hosts}
for i, f in enumerate(concurrent.futures.as_completed(futures), 1):
print(f"\r[{i}/{len(hosts)}] scanning...", end="", flush=True)
result = f.result()
if result:
results.append(result)
print()
return results
def write_csv(results, output):
with open(output, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["ip", "hostname", "open_ports"])
writer.writeheader()
writer.writerows(results)
def main():
import argparse
parser = argparse.ArgumentParser(
description="Network scanner — outputs CSV of hosts and open ports")
parser.add_argument("targets", nargs="+",
help="IPs, hostnames, or CIDR ranges")
parser.add_argument("-p", "--ports",
help="Comma-separated ports (default: common ports)")
parser.add_argument("-o", "--output",
help="Output CSV file (default: scan_<timestamp>.csv)")
parser.add_argument("-w", "--workers", type=int, default=50,
help="Concurrent threads (default: 50)")
args = parser.parse_args()
ports = list(map(int, args.ports.split(","))) if args.ports else DEFAULT_PORTS
output = args.output or f"scan_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
print(f"Scanning {args.targets} on {len(ports)} ports...")
results = scan(args.targets, ports, args.workers)
write_csv(results, output)
print(f"Found {len(results)} host(s) with open ports → {output}")
if __name__ == "__main__":
main()