Glen Larkin

Network Scanner — Python Project

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.

Python 3.10+ No External Dependencies CIDR Range Support Concurrent Scanning CSV Output

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

21 — FTP 22 — SSH 23 — Telnet 25 — SMTP 53 — DNS 80 — HTTP 110 — POP3 143 — IMAP 443 — HTTPS 445 — SMB 3306 — MySQL 3389 — RDP 5432 — PostgreSQL 6379 — Redis 8080 — HTTP Alt 8443 — HTTPS Alt 27017 — MongoDB

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()
▲