aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: f411205d4da8de0f1a94c5b88a37726ae5f3306e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 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`, `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

```
abusectl init                 # asks, writes ~/.config/abusectl/config.toml
abusectl parse msg.eml        # -> case directory, IOCs      offline
abusectl contacts <case>      # + abuse contacts via RDAP    network, read-only
abusectl report <case>        # + report bodies              offline
      # review the bodies, by hand or in a mail client
abusectl submit <case>        # MISP, then the vendors       network, writes
abusectl retry                # whatever is due              cron
```

`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.

## 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-<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

```
~/.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.

## 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
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.

**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
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`, `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

```bash
python3 -m unittest discover 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 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

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.