https://github.com/Altis-Technology/rookery
Rookery is a defensive abuse- and malware-detection system for operators of game server hosting panels (Pterodactyl/Pelican). It finds crypto miners, proxy and tunnel abuse, botnet nodes, phishing kits, decompression bombs, container-escape attempts, and panel-feature abuse running on servers the operator hosts — and it can, under strict and deliberate constraints, suspend the servers it proves out.
It is built for free-tier and budget hosts, where abuse arrives within days and upstream abuse complaints arrive after the damage is done.
Everything Rookery does follows one pipeline:
collect → detect → score → incident → human, or the gate
- Collect. A daemon on each node gathers facts, read-only: files (paths, hashes, sizes — never contents), startup commands, process lists, socket listeners, console transcripts. Collection never modifies, moves, or deletes a customer file; CI proves it by hashing a fixture tree before and after a full scan.
- Detect. Detectors turn facts into findings. Each finding is one observation tied to one signal — a rule declared in YAML, not code, with a weight, a false-positive risk, a plain-language explanation, and positive/negative fixtures enforced in CI.
- Score. Findings for one server combine into an incident score: repeated sightings deduplicate, high-false-positive signals are capped, and distinct signal families multiply the score. Corroboration is the whole design — one weak signal means little; three independent ones mean a lot.
- Decide. An incident goes to a human (Discord, CLI, logs). If — and only if — the operator has deliberately enabled it, an incident that passes every check of the enforcement gate can suspend the server, after a public countdown with a veto.
Two rules govern everything:
- Unknown is never clean. A detector that cannot evaluate something says so, with the reason. Silence is never reported as safety.
- No single signal escalates — with one documented exception: a high-confidence container-escape signal escalates alone, because a missed escape costs the whole node (ADR-020).
- It does not act without being armed, twice. Automated suspension requires two separate flags (
enforcement.enabledin both the daemon and module configs), a completed shadow-mode period, and a passing gate evaluation. Out of the box, Rookery finds and reports; it touches nothing. - It never deletes, throttles, reinstalls, or modifies a server. The only enforcement verb is suspend, and it is reversible in one call.
- It does not scan third parties. It scans only infrastructure the operator owns, using the operator's own credentials, under a terms-of-service disclosure the operator makes to their users.
- It does not collect file contents. Findings carry hashes, paths, sizes, metric values, and redacted excerpts. File contents never leave the node without an explicit, logged, per-incident evidence request.
- It does not inspect traffic payloads. Network detection works from flow metadata: who talked to whom, on what port, how much, how often.
- It contains no live malware, no exploit code, and no working attack tooling. Detection rules and synthetic fixtures only.
Signals are grouped into families; each family is owned by a detection module. rookery rules list is the authoritative, current list — what follows is the map.
| Module | Family | Catches |
|---|---|---|
| MinerGuard | static, config, runtime | Miners and compute theft: known miner hashes, startup-command drift, runtime-acquired binaries, idle CPU burn, download-and-execute startup commands |
| ArchiveGuard | static | Decompression bombs and quota abuse, detected before extraction — never extract-then-measure |
| TunnelGuard | network, static | Proxies and tunnels: proxy protocol configs (vless/vmess/…), tunnel tokens in env files, proxy cores (xray/sing-box/v2ray), public proxy and Tor listings |
| EgressGuard | network | Outbound abuse: floods, scanning, spam, C2 — flow metadata only |
| EscapeGuard | panel, audit | Container escapes and the node configuration that would allow them: runtime sockets, cgroup writes, privileged containers, missing seccomp/userns, EOL kernels, vulnerable panel/wings versions |
| PanelGuard | panel | Abuse of the panel's own features, from its activity log — needs no agent |
| ServiceGuard | network | Listening services: unallocated ports, exposed datastores, mail/services, excess listeners versus the game's profile |
| ResourceGuard | runtime | Resource attribution: fork bombs, PID/thread exhaustion, restart cycling, excess processes versus the game's profile |
| ConsoleGuard | console | Console transcripts: runtime downloads, shell spawns, log self-deletion, C2 pushes — redacted, chat excluded |
| WorkloadGuard | workload | "Is this actually a game server?" — positive identity scoring against the declared egg; broken installs surface as support findings, not accusations |
| EggGuard | egg | Malicious or drifted egg definitions: installer scripts, images, variables, upstream diffs |
The field-tested reality these encode: abusers control binary names, file locations, and variable names — they cannot control the arguments their tools require, the network behaviour, or the process tree. Every durable signal keys on the second set.
A customer uploads a "plugin pack". Inside plugins/EssentialsX/MyWorlds/usr/bin/ sits a renamed cloudflared. The pieces land like this:
static.tunnel-binaryfires on the binary's class;static.hidden-executablefires on the camouflaged dot-path nesting.plugins/.envholdsCLOUDFLARED_TOKEN=…→static.tunnel-tokenfires. The token value never leaves the node; the finding carries the key name, the path, a digest, and a redacted excerpt (INV-011).- A bare UUID in the same file fires
static.proxy-uuid-contextonly because proxy indicators co-occur — a UUID alone is a config file, and the scanner encodes that. - Three independent families corroborate → the scorer multiplies → the incident lands in Discord as high severity, in plain language an operator can paste into a ticket.
If enforcement is armed and the gate passes, the server is queued for suspension with a printed veto command; the countdown expires; the suspension is verified by reading state back from the panel. Every step is audited.
The raw version of abuse response is a curl that suspends a server. Rookery's version is the same API call reached only through the gate. Same endpoint; completely different safety. All of this is in force whenever enforcement is enabled:
- Off by default, always (INV-009). Two flags, in two files, both required. SIGHUP can never enable it.
- Shadow mode first. The daemon records what it would have suspended until a configured sample of operator-reviewed decisions stays under a false-positive ceiling. No reviewed sample, no live action.
- The gate. Every incident must pass every condition: signals on the eligible list, family diversity or a definitional signal, score and confidence floors, and exclusions — young servers, old accounts, paying customers (via the never-suspend list), recent incident history, allowlisted servers.
- Grace with a veto. An eligible suspension waits out a countdown (default 10m, 2m for critical).
rookery enforce veto <incident-id>cancels it. - Circuit breaker. Ceilings per hour, per day, and per account. A trip halts all automation until a human runs
rookery enforce resume. A rejected panel token trips it too. - The operator overrules. If a human unsuspends a server, the machine stands down for the configured cooldown. It does not argue.
- Kill switch. One file (
/etc/rookery/DISABLE_ENFORCEMENT), statted before every action. Works without a restart, without the panel. - Audit everything. Every queue, execution, veto, trip, and resume lands in a durable audit log with actor, reason, and the gate's per-condition evaluation (INV-006).
Check it any time: rookery enforce status shows the breaker, the grace queue, and shadow progress.
| Surface | Rule | Invariant |
|---|---|---|
| Files | contents never collected; hashes/paths/sizes only | INV-003 |
| Console | findings never carry raw log lines; bounded redacted excerpts | INV-010 |
| Config files | tokens, UUIDs, endpoints redacted at the scanner; fuzzed | INV-011 |
| Owner email | audit trail and operator notification only, never finding evidence | INV-011 |
| Discord | no customer file contents or raw console lines, ever | INV-003 |
Requires Go 1.26 to build; a prebuilt binary drops anywhere.
go build -o rookery ./cmd/rookery
The systemd layout (see deploy/rookery.service for the full unit):
/usr/local/bin/rookery the binary
/etc/rookery/ configuration root (daemon.yaml, rules/, rookery.env)
/var/lib/rookery/ durable state (store, suppressions, grace queue, audit log)
Environment (in /etc/rookery/rookery.env, never in flags or files):
ROOKERY_PANEL_URL=https://panel.example.com
ROOKERY_PANEL_TOKEN=ptla_... # application API, read scope
ROOKERY_PANEL_ADMIN_TOKEN=ptla_... # application API, read+write — only for enforcement
ROOKERY_DISCORD_BOT_TOKEN=... # only for the Discord bot
ROOKERY_DISCORD_PUBLIC_KEY=... # only for the Discord bot
ROOKERY_DISCORD_APPLICATION_ID=... # only for the Discord bot
The read token needs read-only scopes; the admin token is required only when enforcement is enabled, and the suspension client uses exactly two verbs — suspend and unsuspend — with read-back verification.
The daemon is the steady state: it watches the panel for new servers, schedules scans, posts findings, and (when armed) runs the enforcement loop.
rookery start --role all --node 4 --config /etc/rookery/daemon.yaml \
--audit-config /etc/rookery/escapeguard.yaml --notify-config /etc/rookery/notify.yaml \
--collector-config /etc/rookery/collector.yaml --archive-config /etc/rookery/archiveguard.yaml \
--workload-config /etc/rookery/workloadguard.yaml --config-scan-config /etc/rookery/configscan.yaml \
--score-config /etc/rookery/score.yaml
Startup prints a first-run status block: enforcement state, audit outcome, loop cadences, and — by name — every detector family that is not yet wired, with the reason. What the daemon cannot do, it says out loud.
Exit codes are the charter's: 0 clean, 1 findings, 2 usage error, 3 collection failure.
| Command | What it does |
|---|---|
rookery audit [--snapshot <path>] [--json] |
Node configuration audit: is this node one command away from a container escape? Specific remediation per finding. The --snapshot fixtures demo it in sixty seconds, no panel required. |
rookery scan --node <id> [--server <uuid>] [--json] [--notify] |
Scan one node's servers through the panel API: startup drift, static passes, config indicators, workload identity, binary names. --notify posts to Discord. Budgets via --scan-max-files/-bytes/-duration. |
rookery rules list [--json] |
Every registered signal: id, module, family, weight, FP risk. |
rookery rules test |
Validates every rule has positive and negative fixtures. CI runs this; run it after editing rules. |
rookery explain <signal-id> |
The plain-language explanation of one signal, written to be pasted into a support ticket. |
rookery suppress add --signal <id> [--path-prefix <p>] --reason <text> [--expires <d>] |
Narrow a rule that is wrong for your fleet — scoped to a signal, a server, an egg, and optionally a path prefix, with a mandatory reason and author. Suppressed findings are reported as suppressed, never dropped silently. |
rookery suppress list / rookery suppress rm <id> |
Inspect and lift suppressions. |
rookery egg audit <file.json> |
Audit an egg definition: installer privilege, image references, variables, startup. |
rookery egg diff <file.json> |
Diff an egg against its verified upstream. |
rookery dashboard [--ascii] |
Live TUI: egress, gauges, findings, by-module bars. Staleness is always visible. |
rookery bot serve / rookery bot register |
The Discord triage bot and slash-command registration (below). |
rookery enforce status |
Breaker state, pending grace queue, shadow progress. |
rookery enforce veto <incident-id> |
Cancel a queued suspension. |
rookery enforce resume |
Re-arm a tripped breaker — a deliberate human act, audited. |
rookery version |
Version. |
Most small hosts live in Discord; the bot is the triage surface. It is a client of the core, never an enforcement path — it recommends and records.
export ROOKERY_DISCORD_BOT_TOKEN=... ROOKERY_DISCORD_PUBLIC_KEY=... ROOKERY_DISCORD_APPLICATION_ID=...
rookery bot register --notify-config /etc/rookery/notify.yaml # once per guild
rookery bot serve --notify-config /etc/rookery/notify.yaml # the interactions endpoint
Point the Discord application's Interactions Endpoint URL at the bot's public HTTPS URL (/interactions). Slash commands: /incident view|list, /queue, /server info, /stats (dismiss and exception rates per signal — the misconfiguration early-warning), /explain, /audit run. Alerts are idempotent across restarts, batched under storm conditions, and ephemeral wherever they carry customer identifiers.
Rookery expects to be calibrated against your fleet. The tools, in order of preference:
- Suppress with a reason.
rookery suppress add --signal … --path-prefix … --reason …. A suppression with no reason is a lie you tell yourself; the CLI requires one./statsin Discord shows dismiss and exception rates per signal — a signal dismissed 90% of the time is misconfigured. - Adjust thresholds in
config/*.yaml. Every value is documented as a starting point requiring calibration. - Allowlist agents (
config/allowed-agents.txt) and never-suspend servers (config/never-suspend.txt). - Write a rule. Signals are YAML in
rules/signals/with fixtures inrules/fixtures/signals/<id>/{positive,negative}/; the fixture gate fails the build without both. Seedocs/writing-rules.md.
| File | Owns |
|---|---|
config/daemon.yaml |
Roles, state dir, health endpoint, watcher/scheduler cadences, scrutiny window, queue fairness, enforcement enable flag |
config/enforcement.yaml |
The gate: signal lists, thresholds, exclusions, shadow, grace, breaker, kill switch, cooldown |
config/collector.yaml |
Collection bounds: content dirs, static scan sizes |
config/archiveguard.yaml |
Archive scan limits |
config/configscan.yaml |
Config-indicator scan: indicator data paths, depth, size, egg and agent exclusions |
config/workloadguard.yaml |
Identity thresholds, minimum markers, egg-family exclusions |
config/score.yaml |
Score bands, diversity multipliers, decay |
config/notify.yaml |
Discord routing by severity and module |
config/resourceguard.yaml, serviceguard.yaml, service-policy.yaml, console.yaml, eggguard.yaml, escapeguard.yaml, minerguard.yaml |
Per-module detection tuning |
cmd/rookery the CLI (every command above)
internal/domain core types, signal registry, egress validation
internal/detect detector implementations by signal family
internal/scan the scan engine shared by CLI and daemon
internal/daemon the long-running process: loop, queue, scheduler, health
internal/enforce the gate, the executor, the lists
internal/collect node-side read-only gathering
internal/panel the Pterodactyl API client (read + suspend/unsuspend only)
internal/bot the Discord bot
internal/store sqlite: incidents, suppressions, grace queue, breaker, audit
internal/score scoring, decay, suppressions
internal/rules YAML rule loading and the fixture gate
pkg/guard standalone streaming decompression guard
pkg/tunnelcheck standalone proxy/Tor listing checker
rules/ shipped detection data: signals, indicators, name lists, fixtures
testdata/ synthetic corpora (traces, archives, listings, snapshots, fixtures)
config/ module configuration files
deploy/ the systemd unit
docs/ hardening, operating, and rule-writing guides
The pre-commit gate, also run in CI on every push and pull request:
gofmt -l .
go vet ./...
golangci-lint run
go test ./...
gitleaks detect --no-banner
Commits are atomic and conventional (feat, fix, refactor, test, docs, chore, ci, perf). Every detection rule change includes its fixtures. No secrets, real customer data, or live malware — ever. Design decisions are ADRs; operational history is the context tree.
Rookery is defensive tooling for infrastructure the operator controls. The repository contains no live malware samples and no exploit code, only detection rules and synthetic fixtures.
Report vulnerabilities through GitHub private vulnerability reporting. Please do not open public issues for security reports. A code path that mutates customer data or bypasses the enforcement gate is a security bug in this project by definition — report it as one.
For support, join our discord: https://discord.gg/q3bgy7AY2k
MIT. See LICENSE.





