Skip to content

safety: guard against fake releases; fix config/indexer blind spots - #5

Merged
Harrsn merged 2 commits into
mainfrom
fix/fake-release-guards
Sep 17, 2026
Merged

Harrsn merged 2 commits into
mainfrom
fix/fake-release-guards

Conversation

@Harrsn

@Harrsn Harrsn commented Sep 17, 2026

Copy link
Copy Markdown
Owner

What happened

On Sept 16, two things broke, and each hid the other.

1. Search and the hunter lost the Jackett key (13:27, the PR #2 deploy).

  • JACKETT_API_KEY had only ever lived in the old Portainer stack environment. The tracked deploy/stack.yml deliberately carries no secrets, and faucet.env never had the key, so the recreated container came up without it.
  • The UI's only symptom was "Search failed: 502". api_search turns every SearchError into a 502, and jget threw away the message.
  • The dashboard's Indexer pill stayed green because it was wired to client_ok.
  • Settings had no field for the key: index.html hard-coded jackett_api_key: null.
  • Once the key was saved (via a bookmarklet), search worked but the hunter didn't. scheduler.py and stalls.py bind config at import, and config.reload() swaps in a new object (F15).
  • Every hunt search failed silently. Hunt failures were never logged (F33).

2. Bait releases for South Park S29E01, minutes before the 10 p.m. premiere.

  • The first bait was South Park S29E01 South America 1080p WEB-DL x265 NTb.exe, a 1 GB Windows executable.
  • The second was a bare magnet, South+Park+S29E01+…+NTb, whose name carried no extension until its metadata arrived.
  • The sorter only files video, so the downloads "vanished": reported as sorted, then deleted by REMOVE_ON_COMPLETE.
  • Meanwhile the partial .exe sat on the NAS share while it downloaded.
  • The hunter would have grabbed one too: TMDb lists the air date as that day, so reconcile marked the episode missing before it had aired.

What changed

Fake-release defenses (faucet/safety.py, new)

  1. Name filter. search.search() drops results whose title is an executable (.exe, .scr, .msi, .lnk, .js, .ps1, .apk, …; .com is excluded because of tracker watermarks). Every path goes through it: the search page, the hourglass, packs, the hunter and subscriptions. Results.hidden carries the count, and the UI shows "N unsafe results hidden". BLOCK_EXECUTABLE_RELEASES=0 disables it.
  2. Live payload check. A new _guard_loop runs every GUARD_INTERVAL_SECONDS (60), plus once at the start of each tick.
    • What counts as bait: once a torrent's metadata arrives, a media torrent (the classifier doesn't say "game") whose files are executables with no non-sample video.
    • What happens to it: it's paused, never deleted, and recorded in transfer_checks and as a suspicious history event. It's flagged in Activity → Transfers and on the dashboard, and a notification goes out when failed or suspicious is in NOTIFY_ON.
    • The want: it's re-queued (reusing the stall handler's mapping), and the release stays in grabbed, so it's never picked again.
    • Checked once per (id, name): an admin's resume sticks, client-id reuse after a restart triggers a re-check, and ids that disappear are pruned.
    • Pause failures are not recorded, so the next pass retries.
  3. Sorter backstop. Whatever finishes before the check runs gets EXIT_SUSPICIOUS = 5: in consume mode it's quarantined to _failed/ with every executable renamed *.faucet-blocked. quarantine() now returns the destination path. The hook removes the torrent on rc 5 and records and notifies; the sweep counts 5 as swept.

The stall handler only touches downloading transfers, so a paused, flagged torrent can't be stall-removed.

Air-date delay

  • AIR_DELAY_DAYS (default 1; 0 to 30; editable in Settings → Behavior, with validation). series.hunt_eligible() gates reconcile, the pack pre-pass (both the wanted list and its aired-episode count), and the per-episode hunt.
  • The hunter check matters on its own: wants created before the delay existed aren't hunted early either.

Config and indexer visibility

  • Settings → Connections gets a write-only Jackett API key field with a ✓/✕ chip. "Test connections" uses a typed key, and /api/settings exposes only JACKETT_API_KEY_SET.
  • F15: the scheduler and stall handler call _cfg(), which reads faucet.config.config live.
  • The hunt returns early with one warning when the indexer isn't configured. Search errors are summarized once per pass, and add failures are logged and recorded as grab_failed (F33).
  • Torznab <error> documents (a bad API key comes back as HTTP 200) now raise instead of reading as "no results".
  • /api/search returns 503 with an explanation when unconfigured, and jget shows the server's detail text.
  • Dashboard: the Indexer pill uses indexer.configured, and a warnings banner covers a missing key, a missing or unreachable client, and suspicious downloads. Admin /health includes the same warnings; public /health stays minimal.

Docs

  • README env table: AIR_DELAY_DAYS, BLOCK_EXECUTABLE_RELEASES, GUARD_INTERVAL_SECONDS.
  • HOOKS.md: a new "Fake releases" section, plus exit code 5.

Tests

  • tests/test_fake_releases.py, 41 tests:
    • name cases, including the .com watermark and PS4 .pkg
    • torznab filtering and the error document
    • payload classification
    • the guard: pause, flag, want requeue, resume respected, waiting for magnet metadata, games exempt, pause-failure retry, id reuse
    • sorter quarantine and defusing (directory and single file); seeding bait left in place; a real release with a bundled .exe still filed; games untouched
    • hook rc 5
    • air delay in reconcile and the hunter
    • the missing-key short-circuit; F15 (a key saved through the Settings path reaches the hunter); F33 logging; the search-failure summary
    • API contract: transfer flags, dashboard, /health, settings round-trip and validation, the hidden count
  • Against main, 18 of them fail.
  • tests/test_wants_upgrades.py now uses monkeypatch. Its direct searchmod.search = … assignments leaked a fake search into later files, which only surfaced when test order changed.
  • Full suite: 209 passed, in both file orders. The app boots under uvicorn, logs the unconfigured-indexer skip once, and the guard stays quiet against an unreachable client.

Deploying

  • No migration: transfer_checks creates itself.
  • Existing torrents are checked once on the first guard pass after the deploy, so any executable-only media still seeding gets paused and flagged.

Follow-ups

  • tests/test_faucet.py still assigns SCH.searchmod.search / SCH.make_client directly in five places.
  • Hunter resilience: F13 search storm and backoff (792 wants now), F29 302→magnet (the README claim is still false), F30.
  • Consider having faucet-deploy refuse to recreate the container when JACKETT_API_KEY resolves to empty.

Harrsn and others added 2 commits September 17, 2026 17:27
Fake releases (the S29E01 incident)
- search + hunter drop releases whose name is an executable
  (BLOCK_EXECUTABLE_RELEASES); hidden counts surface in the UI
- live guard (GUARD_INTERVAL_SECONDS, default 60): media torrents whose
  file list is executables with no video are paused, flagged, recorded,
  notified; wants re-queued; never deleted; resumed torrents left alone
- sorter backstop: such releases are quarantined with executables renamed
  *.faucet-blocked; new exit code 5, handled by hook and sweep
- AIR_DELAY_DAYS (default 1): new episodes are not hunted until the day
  after they air; settable in Settings -> Behavior

Config / indexer blind spots
- Settings -> Connections gains a write-only Jackett API key field
- scheduler and stalls read the live config (F15): keys saved in
  Settings reach the hunter without a restart
- hunt short-circuits with one warning when the indexer isn't
  configured; search failures summarized; add failures logged and
  recorded as grab_failed (F33)
- torznab <error> documents (bad API key) raise instead of reading as
  "no results"
- /api/search returns 503 with an explanation when unconfigured; the UI
  shows server error detail instead of a bare status code
- dashboard indexer pill reflects the indexer (it mirrored the client);
  dashboard and admin /health list configuration warnings

Tests
- tests/test_fake_releases.py: 41 regressions
- tests/test_wants_upgrades.py: use monkeypatch (direct assignment leaked
  a fake search into later test files)
@Harrsn
Harrsn merged commit 1fa765e into main Sep 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant