Metadata-Version: 2.4
Name: netscanqt
Version: 0.1.0
Summary: Dual-mode (CLI + PyQt6) async TCP connect scanner
Project-URL: Homepage, https://github.com/scottpeterman/netscanqt
Project-URL: Repository, https://github.com/scottpeterman/netscanqt
Project-URL: Issues, https://github.com/scottpeterman/netscanqt/issues
Author: Scott Peterman
License-Expression: GPL-3.0-or-later
Keywords: asyncio,cli,network,port-scanner,pyqt6
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: X11 Applications :: Qt
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Networking :: Monitoring
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Provides-Extra: gui
Requires-Dist: pyqt6>=6.6; extra == 'gui'
Provides-Extra: rich
Requires-Dist: rich>=13; extra == 'rich'
Description-Content-Type: text/markdown

# netscanqt

A fast, async TCP **connect scanner** with one engine and two front ends: a
streaming command-line tool and an optional PyQt6 GUI. It scans, optionally
discovers live hosts first, and optionally **fingerprints** the services it
finds against the Recog database.

It does not use raw sockets — no root, no `libpcap`, no `CAP_NET_RAW`. It runs
as an ordinary user on a headless jump box, which is exactly where it's meant to
live. The scanning core is pure standard library; the GUI, the prettier CLI
output, and the network features (fingerprint corpus download) are all opt-in,
so a plain `pip install` pulls in nothing you don't need.

> netscanqt is a reachability scanner with service identification, not an nmap
> replacement. It tells you which TCP ports answer and makes a best effort to
> name the service behind them. It does not do SYN/stealth scans, UDP, OS
> fingerprinting, or NSE-style scripting. If you need those, run nmap.

<p align="center">
  <img src="https://raw.githubusercontent.com/scottpeterman/netscanqt/refs/heads/main/screenshots/slides.gif"
       alt="netscanqt GUI: a /24 scan with discovery and service fingerprinting, showing open ports with product and version columns"
       width="640">
</p>

---

## Install

```bash
# CLI + engine — zero non-stdlib dependencies
pip install netscanqt

# add extras as needed
pip install netscanqt[rich]      # prettier CLI tables + progress bars
pip install netscanqt[gui]       # the PyQt6 desktop GUI
pip install netscanqt[gui,rich]  # everything
```

From source, with an editable install for development:

```bash
git clone https://github.com/scottpeterman/netscanqt
cd netscanqt
pip install -e .[gui,rich]
```

Requires Python 3.10+.

Launching:

```bash
netscanqt ...                 # CLI console script
python -m netscanqt ...       # module form, same CLI
netscanqt-gui                 # GUI (prints an install hint without the [gui] extra)
netscanqt-fetch-fingerprints  # download the Recog corpus (see Fingerprinting)
```

---

## Quick start

```bash
# scan the well-known ports across a /24
netscanqt 10.0.0.0/24

# discover live hosts first, then full-scan only those
netscanqt 10.0.0.0/24 -d

# discover, scan, and identify the services on open ports
netscanqt 10.0.0.0/24 -d -F
```

![netscanqt discovering and scanning a /24 from the command line, with a live phased progress bar](https://raw.githubusercontent.com/scottpeterman/netscanqt/refs/heads/main/screenshots/netscan-progress-cli.png)

---

## CLI guide

### Synopsis

```
netscanqt [options] TARGET [TARGET ...]
```

### Targets

One or more hosts, IP addresses, or CIDR blocks, space-separated. CIDR blocks
expand to their usable host addresses; bare IPs and hostnames pass through to
the resolver.

```bash
netscanqt 10.0.0.0/24 192.168.1.1 host.example.com
```

### Ports (`-p` / `--ports`)

| Form          | Meaning                          |
| ------------- | -------------------------------- |
| `22,80,443`   | an explicit list                 |
| `1-1024`      | an inclusive range               |
| `22,8000-8100`| lists and ranges combined        |
| `all` (or `-`)| every port, 1–65535              |

Default: `1-1024`.

### Options

| Flag                    | Default | Description                                                        |
| ----------------------- | ------- | ------------------------------------------------------------------ |
| `-p`, `--ports`         | `1-1024`| ports to scan (see forms above)                                    |
| `-c`, `--concurrency`   | `500`   | number of simultaneous connects                                    |
| `-t`, `--timeout`       | `1.0`   | per-connect timeout, in seconds                                    |
| `-r`, `--retries`       | `1`     | extra attempts on timeout before a port is marked filtered         |
| `--filtered`            | off     | also report filtered ports, not just open ones                     |
| `-d`, `--discover`      | off     | liveness-sweep the range first; full-scan only hosts that answer   |
| `--discovery-timeout`   | `0.5`   | per-probe timeout for the liveness pass                            |
| `-F`, `--fingerprint`   | off     | identify services on open ports (banner grab + Recog match)        |
| `--recog-dir`           |         | path to a Recog `xml/` directory (overrides cache and env)         |
| `--fingerprint-timeout` | `2.0`   | per-service timeout for the fingerprint pass                       |
| `-o`, `--output`        | `text`  | output format: `text`, `json`, or `csv`                            |
| `-q`, `--quiet`         | off     | suppress the progress line on stderr                               |
| `--no-color`            | off     | force plain text even if `rich` is installed                       |
| `--version`             |         | print version and exit                                             |

### Discovery (`-d`) — and why it matters

On a sparse range, most addresses are dark, and without discovery every port on
every dead host is probed until it times out — a `/24 × 1-1024` is ~260,000
connects, almost all waiting on silence.

With `-d`, netscanqt first sweeps a small set of liveness ports
(`22, 80, 443, 445, 3389`) with a short timeout, keeps only the hosts that
respond, then full-scans just those. A host counts as **alive if any probe is
open _or_ closed** — a closed port (an immediate RST) proves a host is there
just as well as an open one; only silence means dead. On a typical sparse `/24`
that turns hundreds of thousands of probes into a few thousand, and minutes into
seconds. Use it whenever you're scanning a range rather than a known host list.

### Tuning for a fast, reliable LAN

```bash
netscanqt 10.0.0.0/24 -d -t 0.4 -r 0
```

`-t 0.4` shortens the wait on non-responders; `-r 0` drops the retry so each
dead probe costs one timeout instead of two.

> **File descriptors.** `--concurrency` is bounded by your open-file limit
> (`ulimit -n`, often 1024). Push past it and the OS refuses sockets; netscanqt
> treats that refusal like an unreachable port, so you'd get *wrong* results,
> not an error. If you raise concurrency, raise the fd limit with it.

---

## Fingerprinting (`-F`)

`-F` adds a second pass over the open ports: it connects, grabs an identifying
string (an HTTP `Server` header, a self-announced banner), and matches it
against the [Recog](https://github.com/rapid7/recog) fingerprint database to
report a product, version, and CPE. The scan stays a clean reachability layer;
fingerprinting is a separate stage layered on top.

### Getting the corpus

netscanqt ships only a tiny built-in sample (a handful of fingerprints), so out
of the box `-F` identifies very little. Download the full corpus once:

```bash
netscanqt-fetch-fingerprints                 # into ~/.cache/netscanqt/recog
netscanqt-fetch-fingerprints --ref v3.1.4    # pin a tag for a reproducible set
netscanqt-fetch-fingerprints --dest ./recog  # somewhere explicit
```

This pulls ~50 XML files (~3 MB) as a single tarball — no GitHub API, so no rate
limiting. Run it once from a box with internet; every scan afterward loads the
corpus locally and offline, which is what keeps `-F` usable on airgapped jump
boxes. The full corpus is ~4,300 fingerprints.

### Where fingerprints are loaded from

Resolved in this order, first match wins:

1. `--recog-dir PATH`
2. `$NETSCANQT_RECOG_DIR`
3. the downloaded cache (`~/.cache/netscanqt/recog`)
4. the built-in sample

So once you've run the fetch command, both the CLI and GUI pick up the full
corpus with no further configuration.

### What it can and can't identify

Self-announcing services are covered: HTTP/HTTPS (Apache, IIS, nginx),
IPP/CUPS, SSH, and the FTP/SMTP/POP/IMAP greeters. Silent or
client-speaks-first services (PostgreSQL, MSSQL, raw 9100) stay unidentified
because they need a real protocol exchange to elicit a response. When no
fingerprint matches, the raw banner is shown instead of a blank — so you always
see what the service actually said.

---

## Output formats (`-o`)

**text** (default) renders a table — a `rich` table with colored states and a
phased progress bar if the `[rich]` extra is installed and you're on a terminal,
or aligned plain text otherwise (also used automatically when piping):

```
Host         Port   State   Service   Product            Version    Latency
10.0.0.27      22   open    ssh       OpenBSD OpenSSH     8.9p1       2.7 ms
10.0.0.55     631   open    ipp       nginx                          54.9 ms
10.0.0.1      443   open    https     Xfinity Broadband ...         153.6 ms
```

![netscanqt rich CLI output: colored port states and a fingerprinted results table](https://raw.githubusercontent.com/scottpeterman/netscanqt/refs/heads/main/screenshots/netscan-report-cli.png)

**json** emits one array after the scan completes; **csv** writes a header plus
rows. With `-F`, both include `product`, `version`, `cpe`, `os`, and the raw
`banner`.

```bash
# discover + fingerprint a /24, pull the SSH hosts out with jq
netscanqt 10.0.0.0/24 -d -F -o json -q | jq '.[] | select(.port == 22)'

# CSV inventory with service identification
netscanqt 10.0.0.0/24 -d -F -o csv -q > inventory.csv
```

### Exit codes

| Code | Meaning                                                             |
| ---- | ------------------------------------------------------------------- |
| `0`  | completed                                                           |
| `2`  | bad input, fingerprint load failure, or GUI launched without `[gui]`|
| `130`| interrupted (Ctrl-C) — sockets are closed cleanly on the way out    |

---

## GUI

```bash
netscanqt-gui
```

Enter targets and ports, tick **Discover live hosts first** and/or **Fingerprint
services**, and hit Scan. Results stream into the table — with Product and
Version columns when fingerprinting — and the progress bar relabels itself
across the discovery, scan, and fingerprint phases. Stop cancels cleanly. It's
the same engine and the same fingerprint corpus as the CLI; the window is just
another way to build a scan configuration.

![netscanqt GUI mid-scan, the progress bar relabeling across the discovery, scan, and fingerprint phases](https://raw.githubusercontent.com/scottpeterman/netscanqt/refs/heads/main/screenshots/netscan-gui-scanning.png)

The corpus the GUI matches against is the same one the CLI resolves, and it can
be inspected and downloaded from the window — the count shown is exactly what
`-F` will match against.

![netscanqt GUI corpus panel: inspect the loaded fingerprint count and download or refresh the Recog database](https://raw.githubusercontent.com/scottpeterman/netscanqt/refs/heads/main/screenshots/recog-gui.png)

---

## Using the engine as a library

The scanner and the fingerprinter are importable on their own; the CLI and GUI
are thin drivers over them. The engine yields structured events and never
prints:

```python
import asyncio
from netscanqt import scan, ScanConfig, ScanResult, ScanProgress
from netscanqt import enrich, load_recog

async def main():
    config = ScanConfig.from_specs(["10.0.0.0/24"], ports="22,80,443", discover=True)
    opens = [e async for e in scan(config) if isinstance(e, ScanResult)]

    recog = load_recog()  # downloaded cache, env, or built-in sample
    async for er in enrich(opens, recog):
        product = er.fingerprint.get("service.product") if er.fingerprint else er.banner
        print(f"{er.result.host}:{er.result.port}  {product}")

asyncio.run(main())
```

`scan()` and `enrich()` are async generators. Stop iterating, `break`, or
`aclose()` and in-flight probes are cancelled and their sockets closed —
cancellation is part of the contract, which is why both front ends get a clean
Stop for free.

---

## How it works

**One engine, two drivers.** `engine.py` knows how to scan and nothing else — no
formatting, no argparse, no Qt. Both shells exist only to construct a
`ScanConfig` and consume the events `scan()` yields. A CLI flag and a GUI
checkbox are the same thing: a field on the config.

**Concurrency, not parallelism.** Connect-scanning is I/O-bound — every worker
waits on the network, not the CPU — so the engine uses a pool of asyncio workers
over a single thread. Threads or multiprocessing would buy nothing; the only way
to go faster is to stop waiting on dead hosts, which is what discovery does.

**Layered passes.** Discovery, the scan, and fingerprinting are distinct passes
that compose: each consumes the previous one's output rather than complicating
it. Fingerprint matching is a pure function, so it's the one piece that could
move to a process pool if service-identification volume ever made regex matching
a CPU bottleneck.

---

## Notes

Scan only hosts and networks you own or are explicitly authorized to scan.
Unauthorized port scanning may be illegal where you are.

The Recog fingerprint database is © Rapid7 and contributors, licensed under
BSD-2-Clause; it is downloaded at the user's request and is not redistributed
with netscanqt.

## License

GPL-3.0-or-later.