# abusectl Abuse reporting for phishing mail. Parses a flagged message, extracts its indicators, resolves who to report each one to, and files the result to a MISP instance and to public abuse channels. **Status: `init` and `parse` are built.** The rest of the pipeline is designed but not written, see `docs/specs/2026-09-08-abusectl-design.md`. Nothing here submits anything to anyone yet. ## What it does ``` abusectl init # asks, writes ~/.config/abusectl/config.toml abusectl parse msg.eml # -> case directory, IOCs offline abusectl contacts # + abuse contacts via RDAP network, read-only abusectl report # + report bodies offline # review the bodies, by hand or in a mail client abusectl submit # MISP, then the vendors network, writes abusectl retry # whatever is due cron ``` `init` and `parse` exist today. Each subcommand runs on its own and is useful on its own: `parse` triages a message with no keys configured at all, and `parse` plus `contacts` plus `report` will produce a document you can send by hand before any API key exists. State lives in a **case directory** rather than in memory, so a review can take a week and survive a reboot. ## Getting started ```bash abusectl init # asks, writes the config abusectl parse message.eml # prints the case directory it created ``` `init` needs to know which `Received` hops your own mail infrastructure adds, because everything below that boundary was written by whoever was talking to your server and can be forged. Three ways to answer, at one prompt: - type the CIDRs, if you know them - name a provider: `gmail`, `fastmail`, `proton`, `outlook`, `privateemail`, `zoho`, whose published sending ranges ship with the tool - point it at a message you know arrived legitimately with `--from-sample good.eml` and pick your own hops out of the real chain `parse` refuses to run until that boundary is set. Guessing it wrong means reporting an innocent third party, so it does not guess. For scripted or agent-driven setup, every question is also a flag and nothing prompts: ```bash abusectl init --non-interactive --trusted-relays 192.0.2.0/24 198.51.100.0/24 abusectl init --non-interactive --provider fastmail ``` Re-running `init` shows what is already configured and asks before replacing it. Either way the old file is copied to `config.toml.bak-` first, and any section this run does not set, such as `[misp]`, is carried across untouched. ## What a case looks like ``` ~/.local/share/abusectl/2026-09-08-a3f1/ source.eml the original, unredacted manifest.json IOCs, and later contacts and per-destination status bodies/ report bodies, once `report` exists ``` `manifest.json` after `parse`: ```json { "id": "ioc-1", "type": "ipv4", "value": "203.0.113.99", "origin": "received-chain", "confidence": "boundary-hop" } ``` Every indicator says where it came from. An IP from the trust boundary is `boundary-hop`, the one address that can be stood behind; anything below it is `untrusted-hop`, recorded because it may be useful but never presented as fact. ## Two properties that are not negotiable **Recipient identifiers are never captured.** Not the `To`, `Cc`, `Delivered-To` or `X-Original-To` headers, not your Message-IDs, not maildir paths or account names. They are dropped at extraction rather than stripped at submission, so the tool cannot disclose an identifier it was never given. Tracking tokens inside URLs count: parameter values are redacted, parameter names and paths are kept, because the names fingerprint the kit and the values identify you. **Nothing remote is ever fetched while parsing.** Not the URLs, not the redirect chains, not remote images. Following a link confirms your address is live to the sender and fires exactly the tracker the message wanted. Redirect chains are read from headers and link text, never by following them. ## Reversible and irreversible `submit` writes to MISP first and stops if that fails. MISP is your own instance and is correctable; a report to AbuseIPDB, URLhaus or VirusTotal cannot be recalled. The reversible step gates the irreversible ones, and it is also what answers "have I reported this infrastructure before". Every destination carries its own status, so a retry sends only what failed. A rate-limited vendor does not mean re-reporting to the three that accepted. ## Design `docs/specs/2026-09-08-abusectl-design.md` is the umbrella design. Each part gets its own spec before it is built. ## Requirements Python 3.12 or newer, and nothing else for what is built today: `init` and `parse` are standard library only. `contacts` will need an HTTP client and `submit` will need PyMISP, so a venv in the checkout is the development arrangement; packaging comes once the tool does something worth installing. ## Tests ```bash python3 -m unittest discover tests ``` 100 tests, no framework, no network. Two of them are not ordinary unit tests and are the ones worth knowing about. The `Received`-chain test is mutation-checked: walking one hop too far makes it report an innocent party named in a header the attacker wrote, and the test fails if that regresses. And the parser's suite passes with `socket` disabled entirely, so "nothing is fetched" is verified rather than documented. ## License GPLv2. See `LICENSE`. ## Development Approach This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback. All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions. The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.