aboutsummaryrefslogtreecommitdiffstats
path: root/docs
AgeCommit message (Collapse)AuthorFilesLines
32 hoursdocs: correct the Accept header and the dependency claimDanilo M.2-6/+11
The spec named a strict Accept: application/rdap+json while the plan, and so the code, sends application/rdap+json with application/json at q=0.9. A spec review caught the disagreement. The wider header is the right behaviour, since some registries serve plain application/json and a strict header would lose a contact that was published, so the spec moves to match the code and now records why. Both documents also claimed requirements.txt stays empty. There is no requirements.txt in this repository and there never has been, so the claim was true only vacuously. They now say the tool declares no dependencies, which is the thing actually being promised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
33 hoursdocs: amend the contacts plan after task 1 to 5Danilo M.1-4/+26
Two corrections found while executing it. The offline proof in task 9 patched socket.socket itself, which breaks the ssl module at import time and produces false failures unrelated to network use. It now patches socket.socket.connect, which is the right target. Task 12 gains a step moving the imports in rdap.py to the top of the file. The module was built one task at a time, so several imports landed mid-file after function definitions. Doing it in the documentation pass keeps it out of the diff that contacts.py builds on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
33 hoursdocs: plan the contacts implementationDanilo M.1-0/+1985
Twelve tasks, TDD throughout, building bottom up: transport, bootstrap cache, server selection, jCard extraction, querying with the label walk, the worklist, resolution, the subcommand, an offline proof, two sweeps and the documentation. The code in the plan was assembled and run before committing it, against the plan's own tests, 44 of which pass. That found two defects in the plan itself: a parameter that shadowed the cache_dir function and forced a globals() lookup, now renamed to cache_root, and a hand-rolled fake request in the redirect test that lacked the attributes HTTPRedirectHandler reads, now a real urllib Request. Also drops the em dashes from the Documents list in AGENTS.md, which the no-em-dash writing rule covers and which the contacts entry had matched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
33 hoursdocs: spec the contacts subcommandDanilo M.1-0/+345
Settles what the umbrella design left open for this part: RDAP bootstrap, caching policy, and what happens for a netblock that publishes no abuse contact. Scope is IPs and domains, with a URL contributing only its host. The bootstrap files are fetched from IANA and cached with a long TTL, while responses are cached in memory for one run only: a stale bootstrap merely misses, but a stale abuse address sends a report into a dead mailbox silently. The registrable domain is found by walking up the labels and asking the registry rather than by bundling a Public Suffix List, which would be a transcribed table that goes stale weekly. Address extraction is a strict abuse-role match with no fallback. In particular it never constructs abuse@<domain>, because for a phishing domain that mailbox belongs to the attacker and mailing it would confirm both the catch and that the address is live. Adds a fourth non-negotiable property: a query carries a bare host or IP and never a URL. Property 1 governs what is published, and a query is a disclosure that appears in no report, so a path segment that parse deliberately flags rather than redacts would otherwise leak. Transport is urllib with an injected fetch, so requirements.txt stays empty and the suite keeps passing with sockets raising, for the whole suite rather than for everything except contacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wrfqr2xqQfhtXCscU7zrdz
2 daysdocs: record the real-mail sweep and open a backlogDanilo M.1-0/+40
A synthetic fixture only ever holds the shapes someone thought of, and the shapes nobody thought of are the ones that leak. Sweeping the user's own tag:spam is the only source of those, and it earned its place: the first message found a header the parser did not read, and the corpus exercised a spoofed Reply-To display name no fixture had. AGENTS.md records it under Testing, beside the fixture rule it is the deliberate exception to: ask first, work from the scratchpad, compare every address in the raw source against the whole ioc output, and let only counts and stripped domains out. A finding becomes a synthetic fixture; the real message never enters the repository. BACKLOG.md opens with the one thing the sweep found that was not worth fixing blind: boilerplate namespace urls (w3.org doctypes) reported as indicators. Noise rather than a defect, and the argument for leaving it alone is real, so it is written down with both sides rather than built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019NHaqA1Rz5ybed7wFUeQbK
2 daysplan: add Namecheap Private Email to the provider tableDanilo M.1-0/+26
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
2 daysplan: take the provider ranges from SPF, not from memoryDanilo M.1-16/+88
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
2 daysplan: implementation plan for init and parseDanilo M.2-2/+2404
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
2 daysspec: retry deferred destinations from cron, not by handDanilo M.1-12/+123
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
2 daysInitial commit: umbrella design for abusectlDanilo M.1-0/+423
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