Sitadel: What You Owe People Once a Distro Ships Your Tool
Sitadel is the oldest thing I still ship. It began as a Python 3 rewrite of an abandoned web application scanner, and at some point — without anyone asking me — it was packaged in Kali Linux and BlackArch. That is the moment the project stopped being mine in the way it had been.
I chose the code. Other people chose the reach. Nothing has taught me more about the distance between building something and it being adopted, and it changed what I consider acceptable to break.
What changes when you are in the image
A user who pip installs your tool made a choice. They read something, compared options, and opted in.
If you break a flag, they will find your changelog.
A user who typed sitadel because it was on the box they were handed made no such choice. They have no
relationship with the project, no reason to read your release notes, and — this is the part that
matters — they are frequently working under time pressure on someone else’s authorised engagement.
Three practical consequences I now hold myself to. Command-line flags are a contract, and adding is cheap while renaming is not. Output formats are consumed by scripts I will never see. And a crash is worse than a missing feature, because a crash mid-engagement costs someone billable time they cannot get back.
The problem it actually solves
A scanner’s job is not finding things. It is producing something a human will act on.
That distinction took me years to properly internalise, and the version I ship now reflects it. Sitadel fingerprints first — server, web and frontend frameworks, WAF, CDN, CMS, language — then runs the attack modules a chosen risk level permits, across bruteforce, injection, known vulnerabilities and information disclosure.
The risk level is a design position, not a convenience. Some checks are noisy and some are genuinely dangerous against a live system, and a tool that cannot express that distinction is a tool you cannot point at production.
The change I would make first on any scanner
Not more detections. Better findings.
Earlier versions produced hits: the same injection reported once per endpoint, with no severity, no proof, and no indication of what to do. Forty-seven lines saying the same thing is not forty-seven times the value — it is one finding plus forty-six units of triage work handed to the reader.
So findings are now deduplicated into groups, carry a confidence level alongside severity, include the evidence that proved them — the payload and the marker in the response — map to a CWE, and come with remediation. Reports export as JSON, HTML or SARIF, and SARIF matters specifically because it is what a CI pipeline already knows how to read. A finding that arrives in a format the pipeline understands gets a ticket; one that arrives as prose in a terminal gets a screenshot in a chat channel.
I did not learn this from writing the scanner. I learned it from ten years of handing teams the OWASP ASVS checklist and watching which artefacts caused work to happen. Then I wrote it into the tool.
The bug that only existed in a real terminal
Version 1.6 added an optional TUI so you can watch a scan as it runs — the crawl tree filling in, findings grouping by class, a progress bar during the attack phase.
Then I pressed q and my shell hung.
The scan runs in a worker thread, which spawns its own thread pool during the attack phase. The TUI framework’s shutdown politely waits for that worker — and there was no mechanism anywhere in the codebase to ask a scan to stop. Exit code zero. Test suite green. Shell unusable until the scan finished on its own.
The fix is cooperative cancellation: a shared event that the quit action sets, which the crawler checks
as it drains its frontier and the attack runner checks between probes and between modules. The scan
unwinds in about a second, and the report’s finally block still writes the partial findings — because
someone who cancels a scan at eighty percent should keep the eighty percent.
Two things I took from this. First, cancellation is a property every long-running loop in a codebase either has or does not, and it is much cheaper to design in than to retrofit. Second, I verified it under a real pseudo-terminal rather than a mock, because the entire bug lived in the interaction between a terminal, a framework’s shutdown sequence and a thread pool. A mock would have passed.
The work nobody asks for
The most recent release is a structural cleanup with no new features and no behaviour change. It is the kind of work that is easy to defer forever, so here is what was in it and why it was worth a version number.
A file-handle leak: wordlists opened without a context manager and never closed, across two bruteforce modules. Every fingerprint plugin was being instantiated twice per scan, because the runner built the object once to register it and again to call it. A dead attribute on the request object that nothing read. And injection detectors were re-compiling their regexes on every response, per payload, per target — so the SQL module ran nine patterns and the LDAP module twenty sequential searches, over and over, when both could be compiled once at import.
None of that is visible to a user. All of it is the difference between a codebase you can still change in year eight and one you cannot.
Where the judgement is
Eight years in, the decisions that come up most are not about detection logic.
Say no to modules. The plugin system exists so a check can be added without forking, which means the right answer to most feature requests is a plugin rather than a core change. Every module in core is something I have to keep working.
Prefer the boring dependency. This tool gets installed on machines I will never see, by a package manager I do not control, at a Python version I did not pick. Every dependency is a way for it to fail in an environment I cannot reproduce.
Fix the thing that makes the next fix cheaper. That is the whole justification for 1.6.1, and it is the argument I find hardest to make to anyone who is measuring output in features.
Python 3.11+, GPL-3.0, 179 commits, and it still gets a release when it needs one. The ASVS checklist is still maintained too — and ten years on, the most-used thing I have ever written is a spreadsheet. Worth remembering the next time I feel like building a platform.