Free sample, in full
Five findings you can check in a minute each.
Everything on this page is free and stays free: the 24 projects out of 60 with no security policy at any standard path, three published routes that do not work, and two that only answer at a path a root-only scraper never tries. Each one comes with the exact command that produced it. The paid file is the other side of the sweep: the 36 rows compiled, $18.
01No route
anywhere
24 projects, five paths each, 120 requests, 120 misses
These publish nothing at SECURITY.md, .github/SECURITY.md, docs/SECURITY.md, lowercase security.md or SECURITY.rst. A reporter landing on the repository has no route from that page. Re-checked 2026-08-10.
Swap the repository name into this and run it against any of them:
$ R=ACINQ/phoenix
$ for p in SECURITY.md .github/SECURITY.md docs/SECURITY.md security.md SECURITY.rst; do \
curl -s -o /dev/null -w "$p %{http_code}\n" \
"https://raw.githubusercontent.com/$R/HEAD/$p"; done
SECURITY.md 404 .github/SECURITY.md 404 docs/SECURITY.md 404 security.md 404 SECURITY.rst 404
Two notes so this is not read as worse than it is. Some of these organisations do run a disclosure process somewhere else, on a vendor page or a bounty platform, and a few of the 24 sit in an org whose other repository publishes a policy. ACINQ is the clean example: ACINQ/eclair publishes a full policy, ACINQ/phoenix carries and references nothing, so a reporter who lands on the wallet has no route from where they are standing. The claim here is narrow and it is the one that matters to somebody holding a bug: nothing at the five paths, on this repository, today.
02Published
but broken
Three routes that exist and still do not work
These three are counted in the 36 because a policy file is there. Reading it is where the trouble starts.
OpenZeppelin/openzeppelin-contracts
The policy routes reports to Immunefi, which works. The problem is what an automated extractor takes out of it: the only email address in the entire file is security@example.com, and it is a template placeholder recommended to downstream projects for their own NatSpec, not a reporting address. Grep the first address out of this SECURITY.md and you get the example domain, which is exactly the failure mode of building a route list by extraction instead of by reading.
$ curl -s https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/HEAD/SECURITY.md \
| grep -n "@"
21: ... We recommend including it in the NatSpec for the contract as `/// @custom:security-contact security@example.com`.
working route: https://immunefi.com/bounty/openzeppelin
WalletWasabi/WalletWasabi
The policy sends anything that does not touch privacy or security to a regular GitHub issue, and the link it gives is to org WalletWasasbi, a transposed s. That org does not exist, so the link is a 404. The file also contains no email address at all: the only working channel is a GitHub private advisory.
$ curl -s https://raw.githubusercontent.com/WalletWasabi/WalletWasabi/HEAD/SECURITY.md | grep -i wasasbi
$ curl -s -o /dev/null -w "%{http_code}\n" https://github.com/WalletWasasbi/WalletWasabi/issues/new/choose
$ curl -s https://raw.githubusercontent.com/WalletWasabi/WalletWasabi/HEAD/SECURITY.md | grep -c "@"
... open a [regular GitHub issue](https://github.com/WalletWasasbi/WalletWasabi/issues/new/choose). 404 0 (zero at signs in the file, so no security email exists)
working route: github.com/WalletWasabi/WalletWasabi/security/advisories/new
solana-labs/solana
The repository is archived and development moved to anza-xyz/agave, but the SECURITY.md is still sitting there answering with 200. It is also still the first result most people reach for when they have a Solana bug, so it keeps catching reports at a desk nobody is standing at.
$ curl -s https://api.github.com/repos/solana-labs/solana | grep '"archived"'
$ curl -s -o /dev/null -w "%{http_code}\n" https://raw.githubusercontent.com/solana-labs/solana/HEAD/SECURITY.md
"archived": true, 200 (policy file still served from an archived repo)
working route: anza-xyz/agave, advisory first then security@anza.xyz, typically acknowledged within 72 hours
03Missed by
root-only
Two that answer, just not where you looked
A scraper that only requests SECURITY.md at the repository root records both of these as having no policy. Both do. This is why the sweep tries five paths and records which one hit.
damus-io/damus
$ for p in SECURITY.md docs/SECURITY.md; do \
curl -s -o /dev/null -w "$p %{http_code}\n" "https://raw.githubusercontent.com/damus-io/damus/HEAD/$p"; done
SECURITY.md 404 docs/SECURITY.md 200
route: jb55@jb55.com, plain email, no PGP published, no SLA stated in the file
BoltzExchange/boltz-backend
$ for p in SECURITY.md security.md; do \
curl -s -o /dev/null -w "$p %{http_code}\n" "https://raw.githubusercontent.com/BoltzExchange/boltz-backend/HEAD/$p"; done
SECURITY.md 404 security.md 200
route: me@michael1011.at, PGP C264 0F63 0570 F5ED EDE0 2DE6 84D2 49BA 7168 5D46, key linked from the policy
04The paid
file
36 rows compiled, CSV and Markdown, $18
Everything above is the shape of the work. The file is the rest of it: every project that does publish a policy, with the fields read out of the file rather than summarised from memory. Header row:
project,repo,path_found,channel,destination,pgp_fingerprint,in_scope,out_of_scope,ack_sla,fix_sla,template_required,bounty,source_url,checked_utc btcpayserver,btcpayserver/btcpayserver,SECURITY.md,email|huntr,...,,"all repos in the btcpayserver org","customised deployments; third party plugins","2 business days", "90 business days, not guaranteed",yes,huntr,https://raw.githubusercontent.com/...,2026-08-10
- One row per project, one file per field. Nothing is merged in from a blog post, a tweet or a vendor page unless the policy file itself points there, in which case the row says so.
- Blank means silent. If a project states no acknowledgement SLA, the cell is empty. It is never filled with a plausible number.
- PGP fingerprints copied verbatim, spaces included, because the spacing is what breaks the paste into
gpg --recv-keys. - The source URL rides on every row, so any claim in the file can be re-read at its origin in one request.
- Scoped differently on request. Only projects that pay a bounty, only Nostr, only firmware, 200 rows instead of 60. Ask in the form and you get a quote before any work starts.
If a row contradicts the file it cites, tell me which one and the $18 comes back. That rule exists because a wrong address is worse than a blank one.
05Pay
$18 flat. Three ways.
Pay by any route, then tell me where to send the files using the form below, or reply on nostr. The CSV and Markdown go out inside the hour.
Disclosure Desk · 36 compiled rows · csv + markdown$18 flat
On chain: send the equivalent of $18 and paste the txid in the form. Card: the Stripe page carries the email you paid from, so a txid is not needed, though a line in the form still helps me match it faster.
06Contact
Tell me where to send it
Paid already, correcting a row, or asking for a different scope. All three go to the same place.
Sent
That is in my inbox. If it was a payment note, the CSV and Markdown come back to that address inside the hour.