| Age | Commit message (Collapse) | Author | Files | Lines |
|
Four hand-written messages using example.org, example.invalid and the
RFC 5737 documentation IP ranges. No real phishing sample goes in this
repository: it would carry the recipient identifiers this tool exists to
keep out of reports, and a repository is potentially public.
forged-chain.eml is the one that matters. The attacker prepends two
Received headers naming an innocent third party, so a parser that walks
past the trust boundary reports 198.51.100.7 rather than 203.0.113.99.
Weekdays verified with date(1) rather than written from memory, since an
RFC2822 parser validates the day against the date and a wrong one is
indistinguishable from a malformed header.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
|
|
Its SPF is a tree of includes rather than a flat list: spf.privateemail.com
carries no addresses at all, only includes, and one branch nests a further
level. Two of the branches live on jellyfish.systems. The entry here is the
flattened union of the five leaf records, deduplicated, 18 networks.
The re-verification command lists the leaves rather than the top-level
name, and a note says why: querying spf.privateemail.com and finding no
ip4 entries looks like a stale record and is not one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
|
|
Every range in the first draft was wrong. They had been written from
memory, and the correct source is each provider's own SPF record, which
is the list of addresses it declares it sends from.
Gmail is the clearest case: the table claimed eleven IPv4 ranges and
_spf.google.com publishes two. Fastmail, Proton and the rest were wrong
in the same way. Outlook and Zoho are added since they were queried
anyway, and the transcription commands are recorded in a comment so the
next check is a copy-paste rather than a search.
Google's goog.json is deliberately NOT the source, though it looks like
one: it lists all Google infrastructure, over a hundred ranges, and using
it would trust every Google-hosted service as part of the user's own mail
path. _spf.google.com is the mail-sending answer.
The table is IPv4 only. An IPv6 hop from one of these providers does not
match and the user is asked instead, which is the safe direction: a hop
wrongly trusted means the real sender is never reported.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
|
|
Thirteen tasks, TDD throughout, stdlib only. parse is pure and offline:
the trusted-relay boundary arrives as an argument rather than a config
read, so the whole extractor is testable against fixtures with no setup.
The plan carries three checks that are not ordinary unit tests. The
Received-chain task has a mutation step, because walking one hop too far
reports an innocent third party named in a header the attacker wrote, and
a test that cannot fail would not protect against it. The URL task runs
the suite with sockets refused, so the never-fetch rule is verified
rather than read. And every fixture is asserted to leave no recipient
address anywhere in the manifest.
init exists because parse refuses to guess the trust boundary. It asks
for CIDRs, offers a static table of known provider ranges, or reads the
chain of a known-good sample and lets the user pick their own hops. A
pure builder with the prompts and the flags as two front ends over it, so
--non-interactive covers agent-driven setup and the config writing is
tested without a terminal. Re-running shows what is already configured
and asks; either route backs the old file up first and preserves sections
this run does not set, so a later init cannot silently drop a MISP key.
The prompts themselves are hand-tested rather than driven from stdin: a
test there would assert the wording it was written against and break on a
rewording that improved it. Task 12 is the checklist, weighted towards
wrong answers.
Redirect chains were missing from the first draft of the plan and are now
specified: a parameter whose value is itself a URL is recovered as an
indicator while every other value stays redacted, which resolves the
conflict between reporting the destination and never publishing a
tracking token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
|
|
A rate limit tells us when, not merely that: a 429 carries Retry-After or
the vendor's reset headers. Recording the deadline and asking the user to
run submit again throws that away and relies on them remembering.
So `abusectl retry` scans every case for destinations whose retry_after
has passed and sends only those, as one unattended cron line beside
mailsync.sh. No inline retry: submit never sleeps waiting for a window,
because a daily quota resets in hours and a process killed while
sleeping is back to the user remembering. One mechanism, not two.
Unattended retry makes three properties load-bearing, since a retry that
re-sends is a duplicate abuse report and cannot be withdrawn. Status is
written before the attempt, so a crash mid-send leaves in-flight, which
is honest, rather than looking like it never happened; retry never
touches in-flight. Attempts are capped, so a dead abuse mailbox stops
being retried. A soft failure with no server deadline gets exponential
backoff.
deferred and failed are separate statuses: deferred means the tool will
handle it, failed means the user must. Collapsing them either strands a
rate-limited report forever or retries a dead mailbox indefinitely.
The qtmaildir dialog accordingly grows no retry button. A deferred
destination belongs to cron, and a button beside it would race the
scheduled run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
|
|
An abuse reporting sidecar for phishing mail: parse a flagged message,
extract its indicators, resolve abuse contacts, and file the result to a
MISP instance and to public abuse channels.
This is the umbrella spec, agreed in one design session. Each part gets
its own spec before it is built; this settles what the parts share and
what would be expensive to change later: the case directory and its
manifest format, the redaction rule, the ordering between MISP and the
vendors, and how partial failure is recorded.
It exists as a separate tool because qtmaildir does no network protocol
work by design, and this needs RDAP, three vendor APIs and mail to abuse
desks. qtmaildir invokes it by name the way it invokes mailsync.sh, and
hosts the review dialog; the two are coupled only by the manifest format
and a command name in config.
Two properties are recorded as safety properties rather than
preferences. Recipient identifiers are never captured, at extraction
rather than at submission, so the tool cannot disclose an identifier it
was never given; tracking tokens inside URLs are covered, since a
parameter value is frequently the recipient's address. And nothing
remote is fetched while parsing, because following a link confirms the
address is live and fires the tracker.
parse is the first part to build: stdlib only, no network, no config,
and its output is the format every other part reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
|