From 1d864197203221fc0dcdd1924d60d1a759fa0d6d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 9 Sep 2026 10:01:48 +0200 Subject: docs: record the fourth property and the contacts command An RDAP query discloses what the user is looking at, and property 1 covers only what is published, so the query rule needed stating in its own right beside the other three. The section records the three leaks it came from, because each one was the same shape: validation applied per branch, forgotten on the next. Also moves rdap.py's imports into one block at the top. Pure move, the suite is 192 either side of it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz --- README.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 15 deletions(-) (limited to 'README.md') diff --git a/README.md b/README.md index 9354ee0..f411205 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ 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. +**Status: `init`, `parse` and `contacts` 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 @@ -20,10 +20,10 @@ 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. +`init`, `parse` and `contacts` 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. @@ -61,6 +61,34 @@ 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. +## Resolving the contacts + +```bash +abusectl contacts ~/.local/share/abusectl/2026-09-08-a3f1 +``` + +`contacts` reads the indicators `parse` recorded and asks the registries who to +report each one to, over RDAP. This is the first command that needs **network +access**, and no API key: RDAP is public. + +It is **read-only**. It queries registries and sends nothing to anyone, so +running it does not report you, or the message, to anybody. A query carries a +bare host or IP address and never a URL, so the path and query string of a +phishing link stay on your machine. Twenty URLs on one host produce one query. + +**A re-run is safe.** It overwrites the contacts in the manifest rather than +merging into them, so a run interrupted halfway leaves nothing half-merged, and +running it again after a registry recovers simply replaces the result. The +indicators themselves are untouched. + +**A missing abuse contact is a normal outcome, not an error.** Many TLDs publish +no RDAP service at all, and a registry that answers may publish a handle with no +`abuse` role on it. Those are recorded with the reason, and the run continues: +one indicator failing must not cost the contacts that did resolve. An indicator +that is not a bare host or IP is kept as unusable, with why, rather than +silently dropped, so you can see during review whether the parser mangled a real +host or the sender planted something. + ## What a case looks like ``` @@ -83,7 +111,7 @@ 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 +## Three 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 @@ -98,6 +126,12 @@ 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. +**A registry query carries a bare host or IP, never a URL.** `contacts` is the +first command that talks to anyone, and an RDAP query tells a third party what +you are looking at. A URL path can carry your identity, and the registrar you +ask may be the attacker's own, so the path, query and fragment of a phishing +link never leave your machine. Only the host does. + ## Reversible and irreversible `submit` writes to MISP first and stops if that fails. MISP is your own @@ -115,10 +149,11 @@ 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. +Python 3.12 or newer, and nothing else for what is built today: `init`, `parse` +and `contacts` are standard library only, `contacts` included, which reaches the +registries with `urllib`. `submit` will need PyMISP, so a venv in the checkout is +the development arrangement; packaging comes once the tool does something worth +installing. ## Tests @@ -126,12 +161,13 @@ arrangement; packaging comes once the tool does something worth installing. python3 -m unittest discover tests ``` -100 tests, no framework, no network. Two of them are not ordinary unit tests +192 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. +And the suite passes with `socket` disabled entirely, `contacts` and its RDAP +code included, so "nothing is fetched" is verified rather than documented. The +network goes in through a `fetch` argument the tests replace. ## License -- cgit v1.2.3