Building a Python interactive shell for DenyHosts analytics
Australians who self-host servers from a home office in Brisbane or run a small cloud deployment in Sydney often spend late nights digging through log files, hunting the IP addresses that have been hammering SSH for the past week. DenyHosts, the Python script written by Phil Schwartz, has long been the first line of defence against these brute-force attempts, quietly curating a deny list in /etc/hosts.deny and pruning old offenders. Yet the data it produces is often underused, because most operators simply trust the daemon and move on.
An interactive shell for exploring DenyHosts data turns that dormant text file into a queryable resource. Instead of grepping through archived logs, you sit at a prompt, type a few characters, and watch the offending hosts scroll by in chronological order. The shell becomes a small-scale security console that fits in a terminal window, even on the modest VPS a hobbyist might rent from a Melbourne datacentre.
The rest of this article walks through the design choices behind such a shell, drawing on Phil Schwartz's broader catalogue of Python tools. It assumes basic familiarity with Python and a working DenyHosts installation.
Why a dedicated shell beats grep
The first question anyone building a Python interactive shell for DenyHosts data has to answer is the obvious one: why not just use grep and awk? The answer lies in state. A REPL keeps the parsed deny list and any auxiliary metadata in memory between commands, so each new question starts where the last one ended. With shell pipelines you re-read the file every time, paying the I/O cost on each invocation.
An interactive prompt can also resolve hostnames once and cache the result, which matters when your machine sits behind a residential connection in Adelaide and the upstream DNS resolver is sometimes sluggish. It can remember which entries you have already triaged, so a follow-up session picks up where the previous one left off.
A structured shell invites extensions too. Once the parser can distinguish a denied entry from an existing one, new commands slot in without breaking the existing ones. For Australian sysadmins who must comply with the Notifiable Data Breaches scheme, that extensibility means the tool can later support audit trails without a rewrite.
Setting up the environment
Before writing a single line of the shell, you need a clean Python environment that does not conflict with the system interpreter DenyHosts itself depends on. A virtual environment created with python3 -m venv is the standard approach and works equally well on a Raspberry Pi sitting in a Perth garage or a workstation in a Canberra office. Activating it isolates ipaddress, configparser, and any future dependencies from the host Python.
The shell will read three primary sources: the live /etc/hosts.deny file, DenyHosts' own working data directory (typically /var/lib/denyhosts), and the archived logs. The cleanest approach is a small adapter class that returns a uniform iterator of DeniedHost records regardless of where the underlying bytes live.
Configuration comes next. DenyHosts reads its own settings from denyhosts.cfg, and the shell should respect the same paths so both tools stay in sync. Operators who run DenyHosts across regions will appreciate that the shell can normalise timestamps to AEST or AEDT automatically.
Designing the REPL and command parser
At its heart, an interactive shell is a loop that reads a line, splits it into a verb and arguments, dispatches to a handler, and prints the result. Python's cmd module is a serviceable starting point. Subclassing Cmd gives you line editing, history recall, and help text almost for free.
The vocabulary of the shell should mirror the questions a security-minded operator actually asks. The following core commands cover most common workflows.
- show: print entries matching a filter
- search: narrow the filter to a substring or regex
- count: return a numeric tally of matching entries
- top: sort results by frequency or recency
Each verb maps to a handler method that returns a string or a list of records, leaving printing decisions to the framework.
Error handling deserves more care than it usually gets in toy REPLs. A malformed command should print a friendly message rather than crashing the session. For operators who sometimes ssh into their servers from a hotel Wi-Fi network in Darwin with a flaky connection, robustness matters more than elegance.
Querying and visualising blocked entries
Once the basic loop is in place, the interesting work begins. The shell should make it easy to ask questions such as "which subnet has tried the most logins this month?" or "how many unique hosts were denied today?" Answering these requires grouping, sorting, and counting, all of which Python's standard library handles with collections.Counter and itertools.groupby. A handful of pure functions answer most queries without pulling in pandas or numpy, which keeps the shell responsive on older hardware.
Visualisation is optional but transformative. A small wrapper around the curses library can render a sparkline of daily deny events, giving an at-a-glance sense of whether an attack is ramping up or fading. For terminals that do not support curses, ANSI colour codes offer a lighter alternative.
The shell can also enrich entries with external context. A reverse DNS lookup, a whois query, or a check against a public block list turns a bare IP address into something a human can interpret. Rate-limiting those calls is essential, both out of politeness to the upstream service and to keep the prompt responsive.
Packaging and sharing the tool
A shell that lives only on its author's machine is a missed opportunity. Packaging it with a pyproject.toml file makes it installable through pip, so a colleague in Melbourne can run pip install denyhosts-shell and start exploring their own data within minutes.
Documentation deserves as much attention as the code. A README that shows three or four example sessions is worth more than a long reference manual, because new users tend to learn by imitation. Including the licence, a short note about DenyHosts' GPL terms, and a contact address rounds out the package.
The shell also slots naturally into the wider Australian open-source scene. A few practical channels for sharing and gathering feedback include:
- linux.conf.au and PyCon AU lightning talks
- the Australian Python Users mailing list and local meetups
- GitHub releases tagged for downstream packagers
- a short blog post or status update on a personal site
Maintenance habits worth keeping
A few habits keep the shell useful long after the initial excitement wears off. Logging every command to a file under /var/log makes it possible to reconstruct a session during a post-incident review, which is exactly what the Australian Cyber Security Centre recommends for small organisations. Rotating those logs with logrotate prevents them from filling the disk on a long-running VPS.
Schema drift is the other silent killer. When DenyHosts changes its internal format, the shell's parser must adapt, or it will start returning empty results that look plausible. Pinning the DenyHosts version in the documentation and adding a small compatibility check at startup catches most mismatches early. Treating the shell as a living tool, rather than a finished artefact, is what separates a side project from a piece of infrastructure worth keeping.
