aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md66
1 files changed, 51 insertions, 15 deletions
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 <case> # 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-<timestamp>` 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