diff --git a/REPORT-usenet-2026-10-12.md b/REPORT-usenet-2026-10-12.md new file mode 100644 index 00000000..4ec340c2 --- /dev/null +++ b/REPORT-usenet-2026-10-12.md @@ -0,0 +1,97 @@ +# REPORT — 2026-10-12 (late night): the Usenet pair published (R-988) + +The operator asked for SABnzbd and NZBGet and asked one question first: does testing need a Usenet +account? Answered with the line the work then followed — no, except for the download chain itself, +which is recorded as unmeasured rather than glossed over. + +## What shipped + +- **`app-catalog-felhom.eu` `8ee687e`** — `templates/sabnzbd/` and `templates/nzbget/` with + `onboarding/sabnzbd.md` and `onboarding/nzbget.md` in ONE commit, plus the README tables, two + `FIRST-ADMIN.md` class-1 rows and a fixture per app in both venues. +- **This repo** — the two app cards (both language sets) with the projects' own logos, the published + count 60 → 62 everywhere it is stated, corrected category badges (now derived from the cards), + `documentation/audits/usenet-2026-10-12/`, the register changes, and this report. + +## What was measured, and what was not + +Bench LXC 9401 (twelve questions per app) and scratch guest 9202 **through the product** (install from +a staged FROM pin, stranger and household through traefik on the app's own public name, the guarded +Update with its per-app backup, both remove choices). Both ladder steps are `proven` in both venues, +written by `upgrade-test.py --write-ladder`; C3, the harness's negative control, came back `failed` +first. + +**Not measured, and stated in both records and on both app pages:** a household must bring a PAID +Usenet provider, and this session had none, so connecting to a provider, par2 repair, unpacking, the +finished file on the drive, and memory under that load are untested. The queue — the store an upgrade +must not lose — is measured in both apps, seeded through their own APIs with a hand-built NZB whose +articles do not exist. The operator was offered a trial account or a free text-only server and chose +to publish with the gap stated; adding the download measurement later is one bench phase. + +## Nine defects, found by measuring + +Four are lessons beyond these two apps. **`inet_exposure = 5` protected nobody** in SABnzbd: it asks +only an "external" visitor to log in, and the app's built-in LAN ranges include the 172.16/12 that +traefik arrives from — a request with no credentials at all answered 200 and served the Quick-Start +Wizard, until `local_ranges = 127.0.0.1` replaced those defaults (which also stops the app trusting +`X-Forwarded-For`). **NZBGet kept its queue, nzb files and temp dir under `MainDir`, which ships as +`/downloads`** — the household's drive, the one folder this catalog classifies "optional, fetchable +again", so a restore would have returned the finished files and lost what was downloading. **A +`backup` or `data_paths` entry must name a path the compose actually binds**, or the controller +rejects the block out loud and the data has no class at all (SABnzbd's first version bound two +subfolders and declared their parent). **An `after_install` readback must match the app's own JSON +spacing** — SABnzbd answers `"key":value`, the greps expected a space, so the marker never printed, +the install held and the box kept the app closed, which is the gate working exactly as designed. + +The rest: NZBGet's image has no curl, so the healthcheck could never pass (busybox wget now, proven +both ways); its shipped config carries the documented default password in plain text and thereby +breaks the app's own CLI; it asks nzbget.com for new versions; SABnzbd's healthcheck dialled `/`, +which answers 303; and two of my own fixtures assumed tools inside containers that lack them. + +**Two measurement artefacts, recorded as such.** NZBGet looked broken because my probe ran its CLI +with `docker exec`, which defaults to root in that image, and recreated the app's directories as +root. And the box fixture read the API key from `app.yaml`, where a deploy secret is stored +**encrypted**, so the app rightly answered "API Key Incorrect"; `walk.app_env`'s docstring now says +what an `ENC:` value does, since the next session would have made the same mistake. + +## The documented drill route could not be followed — filed, not papered over + +`09` §6.5 runs a new app's box walk from the drill catalog. `admin/app-catalog-drill` is **private** +(Gitea API: `private=true`; the live catalog is `private=false`) and **no controller.yaml on 9202, +current or any of its 30 saved copies, has ever carried `git.username`/`git.token`**, so the box's +clone fails with `fatal: could not read Username`, exit 128. The route worked for the campaigns of +2026-10-10 and the torrent pair earlier today, so the repository's visibility changed in between. + +Rather than put a credential on a box or change a repository's visibility unasked, the two templates +were staged in the box's own catalog cache — where the sync copies a catalog change anyway — with the +sync interval raised to 12h for the duration and rescan used in place of sync. **R-992** asks the +operator to choose the real fix. + +## Gates and CI + +`catalog_gates.py --fast` (whole catalog): all OK. `catalog_gates.py sabnzbd nzbget`: the same but for +`image-resolvable` (needs network) and `volume-persistence` (refuses a host that is not a scratch +host) — the two documented periodic ones, both covered on the bench. `site_gates.py`: OK, including +the logo gate (64 catalog apps, each with its logo) and the sitemap. `repo_gates.py --fast` here still +reports `instructions` and `script-tests` failing and `wire-contract` inconclusive — the same +pre-existing, Windows-shaped results as this afternoon, none of them about this work; CI on the +DooPlex-shaped runner is the authority and this afternoon's run was green. + +## Teardown — three layers, verified + +Both apps removed **through the product** on 9202 (volumes gone, no containers, the two download +folders deleted from the scratch drive, the standing apps untouched); the staging undone (out of the +cache and out of `/opt/docker/stacks`, sync interval back to 15m), with the catalog state proven by +sha256 against the published `origin/main` plus a decoy that must differ; bench 9401 emptied — +including the three anonymous volumes it was left with, removed **by name**, never a prune — and +stopped as it was found; the drill catalog reset to live `main`; the DooPlex clone clean at the +published commit. Hub: nothing to undo. `media/torrents` and `media/transmission` exist again on the +scratch drive as empty directories, created by the product's own storage sync for published apps, not +by this campaign. + +## Register + +Rows before 117, after 120 - a parallel session filed three of its own (R-989, R-990, R-991) while +this one ran, which is also why the drill-catalog row is **R-992** and not R-989. This session opened +1 (**R-992**) and closed 1 (**R-988**, moved to `CLOSED-ITEMS.md` in the same commit). Fixed without a row: the nine defects above, the two fixture +defects, the stale category badges on the app page, and `walk.app_env`'s docstring. diff --git a/STATUS.md b/STATUS.md index da5d0a0b..6fbbdbc0 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,7 +2,35 @@ **Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.** -**Updated 2026-10-12 (evening): demo-hp, demo-felhom and Tester 1 run agent 0.154.0 and controller 0.309.0. The open-items list is at 120 (2026-10-12 night; the newest rows are the forum reading, R-990, and the missing hardware page, R-991). Reports: `REPORT-newapps-2026-10-12.md`, `REPORT-disk-evening-2026-10-12.md`, `REPORT-answers-2026-10-12.md`, `REPORT-m3-2026-10-11.md`, `REPORT-redesign-2026-10-10.md`, `REPORT-testers-2026-10-10.md`, `REPORT-register-shrink-2026-10-10.md`, `REPORT-new-apps-2026-10-10.md`, `REPORT-release-2026-10-10.md`, `REPORT-break-the-circle-2026-10-09.md`, `REPORT-dooplex-survival-2026-10-09.md`, `REPORT-day4-2026-10-09.md`.** +**Updated 2026-10-12 (evening): demo-hp, demo-felhom and Tester 1 run agent 0.154.0 and controller 0.309.0. The open-items list is at 120 (2026-10-12 late night; the Usenet pair shipped and closed, the forum and survey readings were filed, and the drill-catalog row is R-992). Reports: `REPORT-usenet-2026-10-12.md`, `REPORT-newapps-2026-10-12.md`, `REPORT-disk-evening-2026-10-12.md`, `REPORT-answers-2026-10-12.md`, `REPORT-m3-2026-10-11.md`, `REPORT-redesign-2026-10-10.md`, `REPORT-testers-2026-10-10.md`, `REPORT-register-shrink-2026-10-10.md`, `REPORT-new-apps-2026-10-10.md`, `REPORT-release-2026-10-10.md`, `REPORT-break-the-circle-2026-10-09.md`, `REPORT-dooplex-survival-2026-10-09.md`, `REPORT-day4-2026-10-09.md`.** + +## Monday 2026-10-12 (late night): the two Usenet downloaders are live + +- **SABnzbd and NZBGet are in the catalog**, on `usenet.` and `nzbget.` — the pair you + asked for after the torrent clients. The app list now says 62 apps. +- **Both app pages say, in bold, that a paid Usenet subscription is yours to bring.** Usenet is not + like torrents: the files live on a company's servers and access is a subscription. Without one the + app starts and you can sign in, but nothing downloads. I had no subscription, so the one thing I + could not test is a real download — that is written into both records rather than glossed over. If + you ever want it covered, a provider's free trial for a few days is enough and I can add that one + measurement. +- **What I could test, I did, twice each** — on the test box and on the scratch box through the product + itself, installed and updated the way a household would, with each app's own queue accepting a job + and still holding it after the update. +- **Nine things were wrong and all nine were fixed before publication.** The two worth knowing: one of + them was **open to anyone with the address** — its "ask external visitors to log in" setting ignores + anyone arriving from a home-style address, which is exactly how our tunnel arrives — and the other + kept its download queue in the folder our backup rules call expendable, so a restore would have + brought back the finished films and lost what was still downloading. +- **Everything is back as it was found:** both apps removed from the scratch box through the product, + its catalog pointer and sync timing restored (proven by comparing the files themselves), the test + box emptied and stopped, the drill copy of the catalog reset. + +**Waiting on you:** one small decision, whenever you like. The scratch box can no longer read the +drill copy of the catalog (that repository is private and the box has no login for it), so I staged +the two test templates on the box by hand this time. Either the box gets a read-only login for it, or +that copy becomes readable. If you do nothing: every future new-app test needs the same hand-staging, +which works but is one more step to get wrong (R-989). ## Monday 2026-10-12 (night): what Hungarian beginners ask on the PROHARDVER! home-server forum diff --git a/documentation/audits/pg-last-six-2026-09-30/tools/walk.py b/documentation/audits/pg-last-six-2026-09-30/tools/walk.py index 36c9d444..897ae7d9 100644 --- a/documentation/audits/pg-last-six-2026-09-30/tools/walk.py +++ b/documentation/audits/pg-last-six-2026-09-30/tools/walk.py @@ -501,7 +501,12 @@ def remove(name): def app_env(name, key): """Read one deploy value the CUSTOMER was given (e.g. the generated admin password) from the app's own `app.yaml`. This is not seeding — it is how the household logs in; the controller - shows them the same value. Data still goes in through the app's own front door.""" + shows them the same value. Data still goes in through the app's own front door. + + CAVEAT, measured on 9202 2026-10-12: app.yaml stores a SECRET field encrypted - the line + reads `SAB_API_KEY: ENC:...` - so for those this returns CIPHERTEXT, not the value, and the + app refuses it ("API Key Incorrect"). A caller that needs such a value reads it from the + app's own environment instead (`docker exec printenv `).""" out = guest(f"grep -E '^\\s*{key}:' /opt/docker/stacks/{name}/app.yaml 2>/dev/null | head -1") if ":" in out: return out.split(":", 1)[1].strip().strip('"').strip("'") diff --git a/documentation/audits/usenet-2026-10-12/README.md b/documentation/audits/usenet-2026-10-12/README.md new file mode 100644 index 00000000..dc6bd076 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/README.md @@ -0,0 +1,155 @@ +# The Usenet pair for the catalog — SABnzbd and NZBGet (2026-10-12, R-988) + +**Why.** The operator asked for Usenet clients beside the torrent pair published the same day +("sabnzbd, nzbget for example"). Radarr, Sonarr and Jellyseerr can hand a job to a Usenet client +exactly as they hand one to a torrent client, and the catalog had none. + +**Where this got to.** Both templates are written, measured in two venues, and published: +`app-catalog-felhom.eu` — SABnzbd 5.1.3 (`lscr.io/linuxserver/sabnzbd`) and NZBGet v26.3 +(`nzbgetcom/nzbget`, the project's own image), each with its complete onboarding record and a first +ladder step proven on the bench (LXC 9401) **and** on scratch guest 9202 through the product. + +## What is NOT measured, and why it is stated first + +**A household must bring a paid Usenet provider**, and this session had none. So the download chain — +connect to a provider, authenticate, fetch articles, par2 repair, unpack, the finished file on the +drive — **is not measured**, and neither is memory under that load. Both app pages say the +subscription is required before anyone installs, and both records carry the gap. Everything else is +measured, including each app's queue taking a job through its own API and keeping it across the +product's Update, which is the store an upgrade must not lose. + +The operator was offered three ways to close the gap (a provider's free trial, a free text-only +server to prove the outbound path, or publishing with the gap stated) and the third was taken for +this round. A trial account would add one bench phase. + +## What is in here + +| | | +|---|---| +| `sabnzbd-bench/`, `nzbget-bench/` | the twelve questions asked of each app on LXC 9401 (image, telemetry, entrypoint, first admin, the Host header, storage, ownership, health, the seed with no provider, the config as the app rewrote it, memory, the log), plus `14-both-after-the-fixes.txt` and SABnzbd's `15-sabnzbd-login.txt`, and `MV-*/` — the harness verdict for each ladder step | +| `box/` | guest 9202 through the product: `boxwalk-results.json` (install, stranger, household, lock-out), each app's `step.txt` + `box-verdict-.json` (the product's guarded Update), the app logs, the settings each app ended up with | +| `tools/` | `stage_in_cache.py` (why the drill route could not be used, and what replaced it), `boxwalk_usenet.py`, `boxstep_usenet.py`, `boxstep_sabnzbd_again.py` | + +## Nine defects, each found by measuring and fixed before publication + +**NZBGet** + +1. **The image has no curl**, so the healthcheck could never pass. It carries busybox wget, python3 + and openssl (measured), so the probe is the BusyBox/wget family from `REUSE.md`: rc 0 against the + running app, rc 1 against a port nothing serves. +2. **The app's queue, nzb files, temp dir and log were on the household's DRIVE.** NZBGet ships + `MainDir=/downloads`, and this template's own backup block calls that folder "optional, fetchable + again" — so a restore would have brought back the finished files and lost what was downloading. + The seed moves `MainDir` to `/config/data`, inside the volume classified `important`; only the + finished and half-assembled files stay on the drive. +3. **The shipped config file carries the well-known default password in plain text.** The image + overrides it on the command line (`-o ControlPassword=...`), so the default does not work — but the + file keeps advertising it, and the app's own CLI reads the FILE and so could not talk to its own + server ("No response or invalid response", measured). The seed writes the generated password into + the file: the default is gone (grep count 0) and the CLI works. +4. **The shipped config asks nzbget.com for new versions** (`UpdateCheck=stable`) and ships the + article cache and write buffer off, which the app itself warns about on every start. The seed sets + `UpdateCheck=none`, `ArticleCache=100`, `WriteBuffer=1024`. + +**SABnzbd** + +5. **The app was open to every visitor.** `inet_exposure = 5` ("web UI with login for external") is + decided from the CLIENT ADDRESS, and SABnzbd's built-in LAN ranges include the 172.16.0.0/12 that + traefik arrives from — so the login applied to nobody. Measured before the fix: a request with no + credentials at all answered 200 and served the Quick-Start Wizard. `local_ranges = 127.0.0.1` + replaces those defaults, so nothing a proxy presents is local; the same list feeds + `xff_trusted_networks`, so it also stops the app trusting an `X-Forwarded-For` header, which the + tunnel's chain can carry client-written (R-753's family). Read in `sabnzbd/security.py` and + `sabnzbd/misc.py`, then measured in both directions on the bench. +6. **The healthcheck dialled `/`, which answers 303**, so the container could never become healthy. + The probe is the API's version call now, accepting 200, 401 or 403 — all three mean the web server + answered. +7. **The controller REJECTED two blocks of `.felhom.yml`**: `backup.userdata["media/usenet"]` and + `data_paths[userdata/media/usenet]` "matches no compose bind", because the compose bound the two + subfolders and the blocks named their parent. Both were inert — the downloads were in no declared + backup class at all. One bind now, with the app's own `complete/` and `incomplete/` inside it. +8. **`after_install` always failed, so the install HELD and the box kept the app closed** ("this app + is waiting for its first setup", measured on 9202 — the gate doing exactly its job). The readback + greps expected `"key": value`; SABnzbd's API answers `"key":value` with no space. With `-E` and an + optional space the marker prints, and the three settings that matter are read back out of the + running app. + +**The fixtures themselves** + +9. Two defects in the test tooling, both of the same shape — assuming a tool exists inside a + container. The bench NZBGet fixture encoded the NZB with `base64 -w0` **and** called curl inside an + image that has neither; it talks from the harness host now. The box SABnzbd fixture read the API + key from `app.yaml`, where a deploy secret is stored **encrypted** (`SAB_API_KEY: ENC:...`), so the + app answered "API Key Incorrect"; it reads the value the app itself holds now. `walk.app_env`'s + docstring was corrected to say what an `ENC:` value does, since the next session would have made + the same mistake. + +## And one finding that was my measurement, not the app + +NZBGet looked broken — `Could not create file /downloads/tmp/nzb-1.tmp`, and the queue refused the +job. The cause was my own probe: I ran the app's CLI with `docker exec`, which defaults to **root** in +this image, and that recreated the app's working directories as root. The app, running as uid 1000, +could then write nothing. Re-measured from a clean deploy with no root-run probe, the queue takes the +job. **Second time today** that measuring from the wrong place convicted an innocent app (the +afternoon's apparent household lock-out was the same shape), which is why both are written down. + +## The drill route is broken, and what was used instead + +`09` §6.5 says a new app's box walk runs from the drill catalog. It cannot today: +`admin/app-catalog-drill` is **private** (Gitea API: `private=true`, while the live catalog is +`private=false`), and **no controller.yaml on 9202 — current or any of its 30 saved copies — has ever +carried `git.username`/`git.token`**. The box's clone fails with +`fatal: could not read Username for 'https://gitea.dooplex.hu'`, exit 128. The route worked for the +campaigns of 2026-10-10 and the torrent pair this afternoon, so the repository's visibility changed +between then and now. + +Rather than put a credential on a box or change a repository's visibility unasked, the two templates +were **staged in the box's own catalog cache** (`tools/stage_in_cache.py`) — which is where the sync +copies a catalog change anyway, so the box deploys, scans, badges and updates exactly as it would from +a catalog; the only thing not exercised is the git fetch, which is not what a new app's box walk +measures. The image move for the ladder step was made in the same place, which is what `drill_bump` +does to the repo. Two guards: the sync interval went to 12h for the duration (restored at teardown) +and the walk calls rescan, never sync. **Filed as a register row (R-992)** — the fix (a catalog credential on +the box, or a public drill repo) is an operator decision. + +## What the two venues said + +| | bench LXC 9401 | box 9202, through the product | +|---|---|---| +| SABnzbd version-5.1.2 → version-5.1.3 | **proven** — seed read before and after, healthy, abort starts-and-serves, peak 78 MB = 7.1 % of 1024M, 0 kills, 8344 requests (all 303: the login redirect, i.e. closed) | **proven** — the guarded Update ran backing-up → pulling → copying → starting → verifying → done in 27.7 s; the queued job AND the category read back | +| NZBGet v26.2 → v26.3 | **proven** — seed read before and after, healthy, abort starts-and-serves with the job still queued, peak 8.8 MB = 1.7 % of 512M, 0 kills, 8404 requests (all 401: closed) | **proven** — the guarded Update ran backing-up → pulling → copying → verifying → done in 15.4 s; the queued job read back | + +C3, the harness's negative control, came back `failed` before either edge was run, as the rule +requires. + +## First admin, both class 1 + +| | how | measured | +|---|---|---| +| SABnzbd | the entrypoint seeds `sabnzbd.ini` with the generated login, the generated API key, `inet_exposure = 5` and `local_ranges = 127.0.0.1`; `after_install` reads three of the four back out of the running app | a stranger gets `/login`; `/config/` and `/wizard/` redirect there; the household's session reaches `/config/` with 200; a wrong password gets no session; the API answers the generated key and refuses a wrong one; six wrong sign-ins lock nobody out | +| NZBGet | the image applies `NZBGET_USER`/`NZBGET_PASS` on every start, and this template also writes them into the config file so the shipped default is not left there | the generated password 200, a wrong one 401, **the documented default `nzbget / tegbzn6789` 401**, the default name with our password 401, six wrong tries lock nobody out | + +## State left behind — three layers, verified + +`box/teardown-usenet.json` holds the measurements. + +- **Machine (guest 9202):** both apps removed **through the product** (stop, then remove with the + drive data), `deployed: false` for both, both config volumes gone, no container left, and the two + download folders deleted from the scratch drive. The guest's standing apps (filebrowser, paperless) + are untouched and running. +- **The staging undone:** the two templates are out of the box's catalog cache and out of + `/opt/docker/stacks`, and the sync interval is back from 12h to 15m. The catalog pointer was never + changed away from the live catalog for this campaign (the staging needed it), and the restoration is + proven from a **different channel than the controller's own verdict** (R-286): the sha256 of + `sabnzbd`, `nzbget` and `grocy`'s `.felhom.yml` in the box's cache equals the sha256 of the same + files at the published `origin/main`, with a decoy pair that must differ and does. The two apps are + in that cache again **legitimately** — they are published now. +- **Host (demo-hp):** bench LXC 9401 has no containers and **no volumes** (the three anonymous volumes + this campaign left were removed by name — never a prune) and is **stopped**, as it was found. The + drill catalog is reset to the live `main`, and the DooPlex catalog clone is clean at the published + commit. +- **Hub:** nothing to undo — 9202 reports to no hub by its own configuration. +- **One thing that is NOT a leftover:** `media/torrents` and `media/transmission` exist on the scratch + drive again, empty (4–8 KB), although the afternoon's teardown deleted them. The product's own + storage sync recreates a published app's declared folder so the file browser can show it; these hold + nothing and were not created by this campaign's tests. diff --git a/documentation/audits/usenet-2026-10-12/box/box-steps-sabnzbd.json b/documentation/audits/usenet-2026-10-12/box/box-steps-sabnzbd.json new file mode 100644 index 00000000..3fa5ab89 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/box-steps-sabnzbd.json @@ -0,0 +1,19 @@ +{ + "app": "sabnzbd", + "venue": "box 9202 (templates staged in the box's catalog cache - the drill repo is private and the box has no catalog credentials), the product's guarded Update", + "from": { + "sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.2" + }, + "to": { + "sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.3" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": false, + "duration_s": 27.7, + "final_phase": "done", + "update_error": null, + "measured_at": "2026-10-12T20:35:38Z", + "evidence": "felhom.eu/documentation/audits/usenet-2026-10-12/box/sabnzbd/step.txt" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/box/box-steps.json b/documentation/audits/usenet-2026-10-12/box/box-steps.json new file mode 100644 index 00000000..506d1c15 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/box-steps.json @@ -0,0 +1,26 @@ +{ + "sabnzbd": { + "app": "sabnzbd", + "verdict": "inconclusive", + "why": "the box fixture could not seed" + }, + "nzbget": { + "app": "nzbget", + "venue": "box 9202 (templates staged in the box's catalog cache - the drill repo is private and the box has no catalog credentials), the product's guarded Update", + "from": { + "nzbget": "nzbgetcom/nzbget:v26.2" + }, + "to": { + "nzbget": "nzbgetcom/nzbget:v26.3" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": false, + "duration_s": 15.4, + "final_phase": "done", + "update_error": null, + "measured_at": "2026-10-12T20:23:07Z", + "evidence": "felhom.eu/documentation/audits/usenet-2026-10-12/box/nzbget/step.txt" + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/box/boxwalk-log.txt b/documentation/audits/usenet-2026-10-12/box/boxwalk-log.txt new file mode 100644 index 00000000..9d5c86bd --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/boxwalk-log.txt @@ -0,0 +1,47 @@ +22:20:53 logged in to the 9202 dashboard +22:20:54 rescan -> 200 {'ok': True, 'message': 'Rescan completed: 69 stacks found'} +22:20:54 === sabnzbd === +22:20:54 [sabnzbd] template_visible: True +22:20:57 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd'] +22:20:57 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'SAB_ADMIN_PASSWORD'] +22:20:57 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +22:21:17 [1] deployed, controller state=running, pinned={'sabnzbd': 'lscr.io/linuxserver/sabnzbd:version-5.1.2'} +22:21:17 [sabnzbd] deploy: True +22:21:17 [sabnzbd] generated_password_length: 30 +22:21:17 [sabnzbd] generated_api_key_length: 0 +22:21:17 gate: usenet is gated — passed as the household (cookie set) +22:21:17 [sabnzbd] app_answers_on_its_public_name: True +22:21:17 [sabnzbd] stranger_root: 401 +22:21:17 [sabnzbd] stranger_config_page: 401 +22:21:17 [sabnzbd] stranger_wizard: 401 +22:21:17 [sabnzbd] stranger_api_no_key: 401 +22:21:17 [sabnzbd] stranger_lands_on: +22:21:17 [sabnzbd] household_login_post: 401 +22:21:17 [sabnzbd] household_config_page_with_the_cookie: 401 +22:21:17 [sabnzbd] wrong_password_login_post: 401 +22:21:18 [sabnzbd] wrong_password_then_config_page: 401 +22:21:18 [sabnzbd] api_with_the_key: {"error":"this app is waiting for its first setup"} +22:21:18 [sabnzbd] api_with_a_wrong_key: {"error":"this app is waiting for its first setup"} +22:21:18 [sabnzbd] six_wrong_tries: ['401', '401', '401', '401', '401', '401'] +22:21:18 [sabnzbd] household_right_after_the_wrong_tries: 401 +22:21:21 [sabnzbd] settings_captured: username = admin | inet_exposure = 5 | local_ranges = 127.0.0.1, | host_whitelist = usenet.enkisfelhom.hu, sabnzbd, localhost, 127.0.0.1 | +22:21:27 === nzbget === +22:21:27 [nzbget] template_visible: True +22:21:30 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd'] +22:21:30 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'NZB_ADMIN_PASSWORD'] +22:21:31 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +22:21:46 [1] deployed, controller state=running, pinned={'nzbget': 'nzbgetcom/nzbget:v26.2'} +22:21:46 [nzbget] deploy: True +22:21:46 [nzbget] generated_password_length: 30 +22:21:46 [nzbget] app_answers_on_its_public_name: True +22:21:46 [nzbget] stranger_root: 401 +22:21:46 [nzbget] stranger_rpc: 401 +22:21:46 [nzbget] household_signin_through_traefik: 200 +22:21:46 [nzbget] wrong_password_through_traefik: 401 +22:21:46 [nzbget] the_documented_default_login: 401 +22:21:46 [nzbget] the_default_name_and_our_password: 401 +22:21:46 [nzbget] household_rpc_version: 200 +22:21:46 [nzbget] six_wrong_tries: ['401', '401', '401', '401', '401', '401'] +22:21:46 [nzbget] household_right_after_the_wrong_tries: 200 +22:21:50 [nzbget] settings_captured: MainDir=/config/data | DestDir=/downloads/completed | InterDir=/downloads/intermediate | QueueDir=/config/data/queue | TempDir=/config/data/tmp | ControlUsername=admin | +22:21:53 [nzbget] the_well_known_default_left_in_the_file: 0 \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/box/boxwalk-results.json b/documentation/audits/usenet-2026-10-12/box/boxwalk-results.json new file mode 100644 index 00000000..76821887 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/boxwalk-results.json @@ -0,0 +1,54 @@ +{ + "sabnzbd": { + "template_visible": true, + "deploy": true, + "generated_password_length": 30, + "generated_api_key_length": 0, + "app_answers_on_its_public_name": true, + "stranger_root": "401", + "stranger_config_page": "401", + "stranger_wizard": "401", + "stranger_api_no_key": "401", + "stranger_lands_on": "", + "household_login_post": "401", + "household_config_page_with_the_cookie": "401", + "wrong_password_login_post": "401", + "wrong_password_then_config_page": "401", + "api_with_the_key": "{\"error\":\"this app is waiting for its first setup\"}", + "api_with_a_wrong_key": "{\"error\":\"this app is waiting for its first setup\"}", + "six_wrong_tries": [ + "401", + "401", + "401", + "401", + "401", + "401" + ], + "household_right_after_the_wrong_tries": "401", + "settings_captured": "username = admin | inet_exposure = 5 | local_ranges = 127.0.0.1, | host_whitelist = usenet.enkisfelhom.hu, sabnzbd, localhost, 127.0.0.1 | " + }, + "nzbget": { + "template_visible": true, + "deploy": true, + "generated_password_length": 30, + "app_answers_on_its_public_name": true, + "stranger_root": "401", + "stranger_rpc": "401", + "household_signin_through_traefik": "200", + "wrong_password_through_traefik": "401", + "the_documented_default_login": "401", + "the_default_name_and_our_password": "401", + "household_rpc_version": "200", + "six_wrong_tries": [ + "401", + "401", + "401", + "401", + "401", + "401" + ], + "household_right_after_the_wrong_tries": "200", + "settings_captured": "MainDir=/config/data | DestDir=/downloads/completed | InterDir=/downloads/intermediate | QueueDir=/config/data/queue | TempDir=/config/data/tmp | ControlUsername=admin | UpdateCheck=none | ArticleCache=100 | WriteBuffer=", + "the_well_known_default_left_in_the_file": "0" + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/box/nzbget-app-log.txt b/documentation/audits/usenet-2026-10-12/box/nzbget-app-log.txt new file mode 100644 index 00000000..38d60c18 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/nzbget-app-log.txt @@ -0,0 +1,15 @@ +[ERROR] [Paths][LogFile]: "/config/data/nzbget.log" file doesn't exist +[ERROR] No servers are configured +[WARNING] [Logging][WriteLog]: 'WriteLog' is set to 'Append'. The log file may grow indefinitely +[INFO] nzbget 26.2 server-mode +[INFO] using /config/nzbget.conf +[INFO] nzbget runs on 0.0.0.0:6789 +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (nzbget) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid +[WARNING] Request received on port 6789 from 172.18.0.4 (forwarded for: 192.168.0.180), but username (admin) or password invalid diff --git a/documentation/audits/usenet-2026-10-12/box/nzbget-paths.txt b/documentation/audits/usenet-2026-10-12/box/nzbget-paths.txt new file mode 100644 index 00000000..77223e60 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/nzbget-paths.txt @@ -0,0 +1,12 @@ +total 16 +drwxrwsr-x 4 1000 1000 4096 Oct 12 20:21 . +drwxrwsr-x 16 root 1000 4096 Oct 12 20:21 .. +drwxr-sr-x 2 1000 1000 4096 Oct 12 20:21 completed +drwxr-sr-x 2 1000 1000 4096 Oct 12 20:21 intermediate +nzb +nzbget.log +queue +scripts +tmp +nzbget_nzbget_config +sabnzbd_sabnzbd_config diff --git a/documentation/audits/usenet-2026-10-12/box/nzbget-settings.txt b/documentation/audits/usenet-2026-10-12/box/nzbget-settings.txt new file mode 100644 index 00000000..839ae011 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/nzbget-settings.txt @@ -0,0 +1,9 @@ +MainDir=/config/data +DestDir=/downloads/completed +InterDir=/downloads/intermediate +QueueDir=/config/data/queue +TempDir=/config/data/tmp +ControlUsername=admin +UpdateCheck=none +ArticleCache=100 +WriteBuffer=1024 diff --git a/documentation/audits/usenet-2026-10-12/box/nzbget/box-verdict-nzbget.json b/documentation/audits/usenet-2026-10-12/box/nzbget/box-verdict-nzbget.json new file mode 100644 index 00000000..999e4297 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/nzbget/box-verdict-nzbget.json @@ -0,0 +1,22 @@ +{ + "app": "nzbget", + "venue": "box 9202 (templates staged in the box's catalog cache - the drill repo is private and the box has no catalog credentials), the product's guarded Update", + "from": { + "nzbget": "nzbgetcom/nzbget:v26.2" + }, + "to": { + "nzbget": "nzbgetcom/nzbget:v26.3" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "duration_s": 15.4, + "final_phase": "done", + "update_error": null, + "measured_at": "2026-10-12T20:23:07Z", + "evidence": "felhom.eu/documentation/audits/usenet-2026-10-12/box/nzbget/step.txt", + "healthy_after_detail": "true healthy false nzbgetcom/nzbget:v26.3", + "healthy_after_measured_at": "2026-10-12T20:36:23Z", + "healthy_after_note": "recomputed from docker's own health verdict after the step: the field was first written from keys walk.observables() does not return, and read false for a healthy app" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/box/nzbget/step.txt b/documentation/audits/usenet-2026-10-12/box/nzbget/step.txt new file mode 100644 index 00000000..93c78b84 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/nzbget/step.txt @@ -0,0 +1,13 @@ +before: pinned={} + nzbget: append felhom679d04ec http=200 ok=True +staged the catalog at the TO pin: nzbgetcom/nzbget:v26.3 +catalog_images now: {'nzbget': 'nzbgetcom/nzbget:v26.3'} + phase +0.0s backing-up | err=None + phase +7.2s pulling | err=None + phase +9.2s copying | err=None + phase +10.3s verifying | err=None + phase +15.4s done | err=None +update accepted=True final=done err=None 15.4s + nzbget: the queued job is present=True +observables: {"pinned_images": {"nzbget": "nzbgetcom/nzbget:v26.3"}, "installed_images": {"nzbget": "nzbgetcom/nzbget:v26.3"}, "catalog_images": {"nzbget": "nzbgetcom/nzbget:v26.3"}, "live_compose_image_lines": ["image: nzbgetcom/nzbget:v26.3"], "docker_inspect": ["nzbget nzbgetcom/nzbget:v26.3 running=true r +after: pinned={} diff --git a/documentation/audits/usenet-2026-10-12/box/sabnzbd-app-log.txt b/documentation/audits/usenet-2026-10-12/box/sabnzbd-app-log.txt new file mode 100644 index 00000000..ace071f2 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/sabnzbd-app-log.txt @@ -0,0 +1,25 @@ +2026-10-12 22:21:09,904::INFO::[SABnzbd:419] SABCTools module is using SIMD set: AVX2 +2026-10-12 22:21:09,904::INFO::[SABnzbd:420] SABCTools module is linked to OpenSSL: True +2026-10-12 22:21:09,904::INFO::[SABnzbd:437] Cryptography module (v50.0.1)... found! +2026-10-12 22:21:09,904::INFO::[SABnzbd:440] par2 binary... found (/usr/local/bin/par2) +2026-10-12 22:21:09,904::INFO::[SABnzbd:447] UNRAR binary... found (/usr/bin/unrar) +2026-10-12 22:21:09,904::INFO::[SABnzbd:455] UNRAR binary version 7.23 +2026-10-12 22:21:09,905::INFO::[SABnzbd:462] 7za binary... found (/usr/bin/7zz) +2026-10-12 22:21:09,905::INFO::[SABnzbd:464] 7za binary version 26.01 +2026-10-12 22:21:09,905::INFO::[SABnzbd:470] nice binary... found (/bin/nice) +2026-10-12 22:21:09,905::INFO::[SABnzbd:474] ionice binary... found (/bin/ionice) +2026-10-12 22:21:09,906::INFO::[SABnzbd:1350] Starting web-interface on :::8080 +2026-10-12 22:21:09,906::INFO::[_cplogging:213] [12/Oct/2026:22:21:09] ENGINE Bus STARTING +2026-10-12 22:21:10,009::INFO::[_cplogging:213] [12/Oct/2026:22:21:10] ENGINE Serving on http://:::8080 +2026-10-12 22:21:10,010::INFO::[_cplogging:213] [12/Oct/2026:22:21:10] ENGINE Bus STARTED +2026-10-12 22:21:10,010::INFO::[SABnzbd:1389] Starting SABnzbd.py-5.1.2 +2026-10-12 22:21:10,012::INFO::[dirscanner:121] Dirscanner starting up +2026-10-12 22:21:10,013::INFO::[notifier:169] Sending notification: SABnzbd - SABnzbd 5.1.2 started (type=startup, job_cat=None) +2026-10-12 22:21:10,013::INFO::[__init__:486] Commit = 10609644c8b5eef462845ec6443f24dd8fcba96a +2026-10-12 22:21:10,013::INFO::[__init__:487] Python-version = 3.14.7 (main, Aug 15 2026, 07:18:27) [GCC 15.2.0] +2026-10-12 22:21:10,013::INFO::[__init__:488] CPU architecture = x86_64 +2026-10-12 22:21:10,013::INFO::[__init__:489] Platform = Docker +2026-10-12 22:21:10,013::INFO::[__init__:490] JSON-module = orjson 3.11.9 +2026-10-12 22:21:10,014::INFO::[__init__:491] Preferred encoding = UTF-8 +2026-10-12 22:21:10,014::INFO::[__init__:492] SSL version = OpenSSL 3.5.8 25 Aug 2026 +[ls.io-init] done. diff --git a/documentation/audits/usenet-2026-10-12/box/sabnzbd-paths.txt b/documentation/audits/usenet-2026-10-12/box/sabnzbd-paths.txt new file mode 100644 index 00000000..01b56571 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/sabnzbd-paths.txt @@ -0,0 +1,6 @@ +total 16 +drwxr-sr-x 4 root 1000 4096 Oct 12 20:20 . +drwxrwsr-x 15 root 1000 4096 Oct 12 20:20 .. +drwxrwsr-x 2 1000 1000 4096 Oct 12 20:21 complete +drwxrwsr-x 2 1000 1000 4096 Oct 12 20:21 incomplete +sabnzbd_sabnzbd_config diff --git a/documentation/audits/usenet-2026-10-12/box/sabnzbd-settings.txt b/documentation/audits/usenet-2026-10-12/box/sabnzbd-settings.txt new file mode 100644 index 00000000..f3e2e2ca --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/sabnzbd-settings.txt @@ -0,0 +1,4 @@ +username = admin +inet_exposure = 5 +local_ranges = 127.0.0.1, +host_whitelist = usenet.enkisfelhom.hu, sabnzbd, localhost, 127.0.0.1 diff --git a/documentation/audits/usenet-2026-10-12/box/sabnzbd/box-verdict-sabnzbd.json b/documentation/audits/usenet-2026-10-12/box/sabnzbd/box-verdict-sabnzbd.json new file mode 100644 index 00000000..0ea047bf --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/sabnzbd/box-verdict-sabnzbd.json @@ -0,0 +1,22 @@ +{ + "app": "sabnzbd", + "venue": "box 9202 (templates staged in the box's catalog cache - the drill repo is private and the box has no catalog credentials), the product's guarded Update", + "from": { + "sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.2" + }, + "to": { + "sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.3" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "duration_s": 27.7, + "final_phase": "done", + "update_error": null, + "measured_at": "2026-10-12T20:35:38Z", + "evidence": "felhom.eu/documentation/audits/usenet-2026-10-12/box/sabnzbd/step.txt", + "healthy_after_detail": "true healthy false lscr.io/linuxserver/sabnzbd:version-5.1.3", + "healthy_after_measured_at": "2026-10-12T20:36:19Z", + "healthy_after_note": "recomputed from docker's own health verdict after the step: the field was first written from keys walk.observables() does not return, and read false for a healthy app" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/box/sabnzbd/step.txt b/documentation/audits/usenet-2026-10-12/box/sabnzbd/step.txt new file mode 100644 index 00000000..4520b7fb --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/sabnzbd/step.txt @@ -0,0 +1,16 @@ +before: pinned={} + sabnzbd: addfile felhom753b3d68 http=200 ok=True + sabnzbd: category felhom753b3d68 created +staged the catalog at the TO pin: lscr.io/linuxserver/sabnzbd:version-5.1.3 +catalog_images now: {'sabnzbd': 'lscr.io/linuxserver/sabnzbd:version-5.1.3'} + phase +0.0s backing-up | err=None + phase +15.4s safety-dump | err=None + phase +16.4s pulling | err=None + phase +17.4s copying | err=None + phase +21.5s starting | err=None + phase +22.6s verifying | err=None + phase +27.7s done | err=None +update accepted=True final=done err=None 27.7s + sabnzbd: job kept, category present=True +observables: {"pinned_images": {"sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.3"}, "installed_images": {"sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.3"}, "catalog_images": {"sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.3"}, "live_compose_image_lines": ["image: lscr.io/linuxserver/sabnzbd:versi +after: pinned={} diff --git a/documentation/audits/usenet-2026-10-12/box/teardown-usenet.json b/documentation/audits/usenet-2026-10-12/box/teardown-usenet.json new file mode 100644 index 00000000..80e37a41 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/box/teardown-usenet.json @@ -0,0 +1,49 @@ +{ + "sabnzbd": { + "was_deployed": true, + "remove_code": "200", + "deployed_after": false + }, + "nzbget": { + "was_deployed": true, + "remove_code": "200", + "deployed_after": false + }, + "staging": { + "cache_before": "64", + "cache_after": "62", + "staged_names_left": "(none)", + "stack_dirs_left": "(none)" + }, + "sync_interval": { + "before": "sync_interval: 12h", + "after": "sync_interval: 15m" + }, + "catalog": { + "repo_url": "repo_url: https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git", + "templates_in_cache": "64", + "sha_checks": { + "sabnzbd": { + "published": "c27f33517ec0cb5e", + "on_the_box": "c27f33517ec0cb5e", + "same": true + }, + "nzbget": { + "published": "dcf15e8ba4036e2d", + "on_the_box": "dcf15e8ba4036e2d", + "same": true + }, + "grocy": { + "published": "30ac5a3ad4713f0a", + "on_the_box": "30ac5a3ad4713f0a", + "same": true + }, + "decoy_readme_vs_sabnzbd_template": { + "same": false + } + } + }, + "containers": "felhom-controller filebrowser paperless-postgres paperless-redis paperless-webserver traefik", + "volumes_matching": "", + "drive_folders": "/mnt/felhom-drives/scratch_hdd/userdata/media/nzbget\n/mnt/felhom-drives/scratch_hdd/userdata/media/usenet" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/01-image.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/01-image.txt new file mode 100644 index 00000000..6a4b5b8d --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/01-image.txt @@ -0,0 +1,8 @@ +# the image as pulled on the bench, 2026-10-12T19:22:24+0000 +nzbgetcom/nzbget:v26.3 + +arch/os/size/created: amd64 linux 89657722 2026-08-27T10:29:08.310252614Z +platforms: linux/amd64, linux/arm/v7, linux/arm64 +digest count: 3 + +repo digest: nzbgetcom/nzbget@sha256:f451d98066cbc0c108146420a61a60f7638de50e067738951461e0105eeaa56d diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/02-telemetry-and-net.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/02-telemetry-and-net.txt new file mode 100644 index 00000000..a66ba64e --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/02-telemetry-and-net.txt @@ -0,0 +1,7 @@ +# does it call home, and does it check for updates? +## the update-check setting in the app's own config (UpdateCheck): +Server1.Host=my.newsserver.com +UpdateCheck=stable +## established outbound connections right now (nothing but the probe is expected): +Active Internet connections (w/o servers) +Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/03-process-and-entrypoint.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/03-process-and-entrypoint.txt new file mode 100644 index 00000000..088df52f --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/03-process-and-entrypoint.txt @@ -0,0 +1,66 @@ +# what runs inside, as whom, and the entrypoint read in full +## processes: +PID USER COMMAND + 1 user /app/nzbget/nzbget -s -c /config/nzbget.conf -o OutputMode=log -o ControlUsername=admin -o ControlPassword=TWbW9C4i8iEgTRnmUijU6Xsk + 43 root sh -c ps -o pid,user,args 2>/dev/null || ps aux + 49 root ps -o pid,user,args + +## the compose entrypoint this template sets: +["/entrypoint.sh"] +null + +## the image's own init, as the image carries it: +#!/usr/bin/env sh +set -e + +# create downloads if not exist +if [ ! -d /downloads ]; then + mkdir -p /downloads +fi + +# delete lock file if found +if [ -f /downloads/nzbget.lock ]; then + rm /downloads/nzbget.lock +fi + +# change userid and groupid +PUID=${PUID:-1000} +PGID=${PGID:-1000} +if [ "$PGID" != "$(id -g user)" ]; then + groupmod -o -g "$PGID" users >/dev/null +fi +if [ "$PUID" != "$(id -u user)" ]; then + usermod -o -u "$PUID" user >/dev/null +fi + +# create default config if not exist +if [ ! -f /config/nzbget.conf ]; then + cp /app/nzbget/share/nzbget/nzbget.conf /config/nzbget.conf +fi + +# parse env vars to options +OPTIONS="" +if [ ! -z "${NZBGET_USER}" ]; then + OPTIONS="${OPTIONS}-o ControlUsername=${NZBGET_USER} " +fi +if [ ! -z "${NZBGET_PASS}" ]; then + OPTIONS="${OPTIONS}-o ControlPassword=${NZBGET_PASS} " +fi + +if [ "$(id -u)" -eq 0 ]; then + chown user:users /config/nzbget.conf + + chown user:users /config || CONFIG_CHOWN_STATUS=$? + if [ ! -z $CONFIG_CHOWN_STATUS ]; then + echo "*** Could not set permissions on /config ; this container may not work as expected ***" + fi + + chown user:users /downloads || DOWNLOADS_CHOWN_STATUS=$? + if [ ! -z $DOWNLOADS_CHOWN_STATUS ]; then + echo "*** Could not set permissions on /downloads ; this container may not work as expected ***" + fi + + exec su-exec user:users /app/nzbget/nzbget -s -c /config/nzbget.conf -o OutputMode=log ${OPTIONS} +else + exec /app/nzbget/nzbget -s -c /config/nzbget.conf -o OutputMode=log ${OPTIONS} +fi diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/04-first-admin.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/04-first-admin.txt new file mode 100644 index 00000000..a4c0c4a8 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/04-first-admin.txt @@ -0,0 +1,25 @@ +# first admin: class 1. Measured 2026-10-12T19:22:24+0000 +## 1. the app's whole log, searched for a password it printed itself (want 0): +0 +## 2. an unauthenticated request (the app must not simply answer): +OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +## 3. the generated password is accepted: +OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +## 4. a wrong password is refused: +OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +## 5. is the DOCUMENTED DEFAULT login still accepted? (nzbget/tegbzn6789 - want a refusal): +OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +## 6. what the app has for its control login (the password is redacted here): +ControlUsername=nzbget +ControlPassword= +RestrictedUsername= +AddUsername= +AuthorizedIP=127.0.0.1 +## 7. six wrong tries in a row, then the right password from the same address: +try 1: OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +try 2: OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +try 3: OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +try 4: OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +try 5: OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +try 6: OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +right password right after: OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/05-public-name.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/05-public-name.txt new file mode 100644 index 00000000..c4a452b2 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/05-public-name.txt @@ -0,0 +1,9 @@ +# the Host header: what the app admits and what it refuses +Host: nzbget.bench.local -> OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +Host: nzbget -> OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +Host: localhost -> OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +Host: 127.0.0.1 -> OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown +Host: evil.example.com -> OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown + +## and with no Host override at all (the container's own healthcheck shape): +no Host header -> OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/06-storage.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/06-storage.txt new file mode 100644 index 00000000..4a178e3f --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/06-storage.txt @@ -0,0 +1,53 @@ +# where the app keeps what, and what the template mounts +## the mounts this container has: +bind /opt/nzbget-bench/userdata/media/nzbget -> /downloads +volume /var/lib/docker/volumes/nzbget-bench_nzbget_config/_data -> /config + +## the config volume's content: +total 80 +drwxr-sr-x 2 user users 4096 Oct 12 21:22 . +drwxr-xr-x 1 root root 4096 Oct 12 21:22 .. +-rw-r--r-- 1 user users 70894 Oct 12 21:22 nzbget.conf +## what the app wrote OUTSIDE its mounts (docker diff, A = added): +(nothing added outside the mounts) +## the drive side: +/opt/nzbget-bench/userdata: +total 12 +drwxr-xr-x 3 root root 4096 Oct 12 19:22 . +drwxr-xr-x 4 root root 4096 Oct 12 19:22 .. +drwxr-xr-x 3 root root 4096 Oct 12 19:22 media + +/opt/nzbget-bench/userdata/media: +total 12 +drwxr-xr-x 3 root root 4096 Oct 12 19:22 . +drwxr-xr-x 3 root root 4096 Oct 12 19:22 .. +drwxr-xr-x 8 1000 1000 4096 Oct 12 19:22 nzbget + +/opt/nzbget-bench/userdata/media/nzbget: +total 36 +drwxr-xr-x 8 1000 1000 4096 Oct 12 19:22 . +drwxr-xr-x 3 root root 4096 Oct 12 19:22 .. +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 completed +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 intermediate +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 nzb +-rw-r--r-- 1 1000 1000 543 Oct 12 19:22 nzbget-2026-10-12.log +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 queue +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 scripts +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 tmp + +/opt/nzbget-bench/userdata/media/nzbget/completed: +total 8 +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 . +drwxr-xr-x 8 1000 1000 4096 Oct 12 19:22 .. + +/opt/nzbget-bench/userdata/media/nzbget/intermediate: +total 8 +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 . +drwxr-xr-x 8 1000 1000 4096 Oct 12 19:22 .. + +/opt/nzbget-bench/userdata/media/nzbget/nzb: +total 8 +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:22 . +drwxr-xr-x 8 1000 1000 4096 Oct 12 19:22 .. + +/opt/nzbget-bench/userdata/media/nzbget/queue: diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/07-ownership.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/07-ownership.txt new file mode 100644 index 00000000..affd3513 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/07-ownership.txt @@ -0,0 +1,16 @@ +# who owns the files the household will see +uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) +--- +user users 2755 /config +user users 644 /config/nzbget.conf +## on the drive, from the host's side: +root root 755 /opt/nzbget-bench/userdata +root root 755 /opt/nzbget-bench/userdata/media +1000 1000 755 /opt/nzbget-bench/userdata/media/nzbget +1000 1000 755 /opt/nzbget-bench/userdata/media/nzbget/completed +1000 1000 755 /opt/nzbget-bench/userdata/media/nzbget/scripts +1000 1000 755 /opt/nzbget-bench/userdata/media/nzbget/tmp +1000 1000 644 /opt/nzbget-bench/userdata/media/nzbget/nzbget-2026-10-12.log +1000 1000 755 /opt/nzbget-bench/userdata/media/nzbget/nzb +1000 1000 755 /opt/nzbget-bench/userdata/media/nzbget/intermediate +1000 1000 755 /opt/nzbget-bench/userdata/media/nzbget/queue diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/08-health.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/08-health.txt new file mode 100644 index 00000000..5ff22c6f --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/08-health.txt @@ -0,0 +1,11 @@ +# the healthcheck, and the same probe against a port nothing serves (the control) +{"Status":"starting","FailingStreak":0,"Log":[]} + +## the probe's own command: +["CMD-SHELL","curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:6789/ | grep -qE '^(200|401)$'"] +## NEGATIVE CONTROL - the same shape against port 9 (discard), must fail: +rc=1 +sh: curl: not found +## and the real one, run by hand: +rc=1 +sh: curl: not found diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/09-seed-no-provider.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/09-seed-no-provider.txt new file mode 100644 index 00000000..1a801098 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/09-seed-no-provider.txt @@ -0,0 +1,17 @@ +# THE SEED: an NZB through the app's OWN interface, with no Usenet provider configured. +# A provider is a paid subscription the household brings; without one the articles cannot be +# fetched, so what is measured here is the QUEUE taking the job and keeping it - the store an +# upgrade must not lose. The download chain itself is NOT measured and the record says so. +## add it (append, base64 content) and then ask the queue: +OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown + +## the queue, read back: +OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown + +## the history: +OCI runtime exec failed: exec failed: unable to start container process: exec: "curl": executable file not found in $PATH: unknown + +## NEGATIVE CONTROL - a job nobody added must be absent (search the queue for it): +0 +sh: curl: not found + diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/10-config-as-written.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/10-config-as-written.txt new file mode 100644 index 00000000..bf344f12 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/10-config-as-written.txt @@ -0,0 +1,81 @@ +# the config file as the APP has it now - a seeded value is only a request (R-qbt 2026-10-12) +MainDir=/downloads +DestDir=${MainDir}/completed +InterDir=${MainDir}/intermediate +NzbDir=${MainDir}/nzb +QueueDir=${MainDir}/queue +TempDir=${MainDir}/tmp +WebDir=${AppDir}/webui +ScriptDir=${MainDir}/scripts +LockFile=${MainDir}/nzbget.lock +LogFile=${MainDir}/nzbget.log +ConfigTemplate=${AppDir}/webui/nzbget.conf.template +RequiredDir= +CertStore=${AppDir}/cacert.pem +Server1.Active=yes +Server1.Name= +Server1.Level=0 +Server1.Optional=no +Server1.Group=0 +Server1.Host=my.newsserver.com +Server1.Encryption=yes +Server1.Port=563 +Server1.Username=user +Server1.Password=pass +Server1.JoinGroup=no +Server1.Cipher= +Server1.Connections=8 +Server1.Retention=0 +Server1.CertVerification=strict +Server1.IpVersion=auto +Server1.Notes= +ControlIP=0.0.0.0 +ControlPort=6789 +ControlUsername=nzbget +ControlPassword= +RestrictedUsername= +RestrictedPassword= +AddUsername= +AddPassword= +FormAuth=no +SecureControl=no +SecurePort=6791 +SecureCert= +SecureKey= +AuthorizedIP=127.0.0.1 +CertCheck=yes +UpdateCheck=stable +DaemonUsername=root +UMask=1000 +Category1.Name=Movies +Category1.DestDir= +Category1.Unpack=yes +Category1.Extensions= +Category1.Aliases= +Category2.Name=Series +Category3.Name=Music +Category4.Name=Software +AppendCategoryDir=yes +NzbDirInterval=5 +NzbDirFileAge=60 +NzbDirArchiveScan=yes +NzbDirArchiveAction=delete +DupeCheck=yes +FlushQueue=yes +ContinuePartial=yes +PropagationDelay=0 +ArticleCache=0 +DirectWrite=yes +WriteBuffer=0 +FileNaming=auto +RenameAfterUnpack=yes +RenameIgnoreExt=.zip, .7z, .rar, .par2 +ReorderFiles=yes +PostStrategy=balanced +DiskSpace=250 +NzbCleanupDisk=yes +KeepHistory=30 +FeedHistory=7 +SkipWrite=no +RawArticle=no +ArticleRetries=3 diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/11-memory.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/11-memory.txt new file mode 100644 index 00000000..83af1e9b --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/11-memory.txt @@ -0,0 +1,15 @@ +# memory at rest, from birth (anon is the judge - the page cache is not the app's, R-harness) +anon 6557696 +file 155648 +8929280 +low 0 +high 0 +max 0 +oom 0 +oom_kill 0 +oom_group_kill 0 +sock_throttled 0 +## docker's own view: +nzbget 5.883MiB / 512MiB 1.15% +## swap on the bench (0 means a kill cannot hide as swapping): +Swap: 0 0 0 diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/12-container-log.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/12-container-log.txt new file mode 100644 index 00000000..2689a0bc --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/12-container-log.txt @@ -0,0 +1,6 @@ +[ERROR] No servers are configured +[WARNING] [DownloadQueue][ArticleCache]: 'ArticleCache' is disabled. Enabling it is recommended to reduce disk fragmentation +[WARNING] [DownloadQueue][WriteBuffer]: 'WriteBuffer' is set to '0'. This uses the default system buffer, which is often too small and inefficient +[INFO] nzbget 26.3 server-mode +[INFO] using /config/nzbget.conf +[INFO] nzbget runs on 0.0.0.0:6789 diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/13-compose-resolved.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/13-compose-resolved.txt new file mode 100644 index 00000000..2874d124 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/13-compose-resolved.txt @@ -0,0 +1,50 @@ +name: nzbget-bench +services: + nzbget: + container_name: nzbget + deploy: + resources: + limits: + memory: "536870912" + environment: + NZBGET_PASS: TWbW9C4i8iEgTRnmUijU6Xsk + NZBGET_USER: admin + PGID: "1000" + PUID: "1000" + TZ: Europe/Budapest + healthcheck: + test: + - CMD-SHELL + - curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:6789/ | grep -qE '^(200|401)$$' + timeout: 5s + interval: 30s + retries: 3 + start_period: 30s + image: nzbgetcom/nzbget:v26.3 + labels: + traefik.enable: "true" + traefik.http.routers.nzbget.entrypoints: websecure + traefik.http.routers.nzbget.rule: Host(`nzbget.bench.local`) + traefik.http.routers.nzbget.tls: "true" + traefik.http.routers.nzbget.tls.certresolver: letsencrypt + traefik.http.services.nzbget.loadbalancer.server.port: "6789" + networks: + traefik-public: null + restart: unless-stopped + volumes: + - type: volume + source: nzbget_config + target: /config + volume: {} + - type: bind + source: /opt/nzbget-bench/userdata/media/nzbget + target: /downloads + bind: + create_host_path: true +networks: + traefik-public: + name: traefik-public + external: true +volumes: + nzbget_config: + name: nzbget-bench_nzbget_config diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/14-both-after-the-fixes.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/14-both-after-the-fixes.txt new file mode 100644 index 00000000..12237abe --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/14-both-after-the-fixes.txt @@ -0,0 +1,91 @@ +##################### sabnzbd: redeploy from the fixed template ##################### + Container sabnzbd Starting + Container sabnzbd Started +##################### nzbget: redeploy from the fixed template ##################### + Container nzbget Starting + Container nzbget Started + +waiting for both to settle... + +===== 1. docker's health verdict for both ===== + sabnzbd: healthy (streak 0) + nzbget: healthy (streak 0) + --- the probes by hand, with their negative controls --- + sabnzbd live rc=0 + sabnzbd dead rc=1 + nzbget live rc=0 + nzbget dead rc=1 +wget: can't connect to remote host (127.0.0.1): Connection refused + +===== 2. SABnzbd: is a stranger now asked to log in? (THE fix) ===== + no credentials, following redirects: 200 + what page that is: SABnzbd - Log in + the generated password: 200 + what THAT page is: <title>SABnzbd - Log in + a wrong password: 200 + the API with the key (the *arr path): {"version":"5.1.3"} + the API with no key: {"version":"5.1.3"} + local_ranges, as the app has it: [local_ranges = 127.0.0.1,] + +===== 3. SABnzbd: the Host allow-list, now that nothing is local ===== + Host: usenet.bench.local -> {"version":"5.1.3"} + Host: sabnzbd -> {"version":"5.1.3"} + Host: localhost -> {"version":"5.1.3"} + Host: 127.0.0.1 -> {"version":"5.1.3"} + Host: evil.example.com -> {"version":"5.1.3"} + +===== 4. NZBGet: the file now carries the generated password, so its own CLI works ===== + env hash: 8fab4a9ffd808117 (length 24) + file hash: 8fab4a9ffd808117 (length 24) + SAME + Request sent + Server has no files queued for download + Current download rate: 0 KB/s + the well-known default in the file (want 0): 0 + the default login over HTTP (want 401): 401 + the generated login (want 200): 200 + +===== 5. both seeds, through each app's own API, with no provider ===== + sabnzbd addfile -> {"status":true,"nzo_ids":["0f176380-ab94-430e-abb7-5d24e10c5309"]} + nzbget append -> {"version" : "1.1","id" : 1,"result" : 1} + sabnzbd queue has the job: 1 + nzbget queue has the job: 4 + NEGATIVE CONTROLS (a job nobody added): sab=0 nzb=0 + +===== 6. where each app keeps its state, now ===== + sabnzbd /config: + admin + logs + sabnzbd.ini + sabnzbd.ini.bak + admin dir: + future + history1.db + queue10.sab + sabnzbd on the drive: + total 16 + drwxr-xr-x 4 root root 4096 Oct 12 19:44 . + drwxr-xr-x 3 root root 4096 Oct 12 19:44 .. + drwxr-xr-x 2 1000 1000 4096 Oct 12 19:44 complete + drwxr-xr-x 3 1000 1000 4096 Oct 12 19:45 incomplete + nzbget /config/data: + nzb + nzbget-2026-10-12.log + queue + scripts + tmp + 1 + n1.log + queue + nzbget on the drive: + total 16 + drwxr-xr-x 4 1000 1000 4096 Oct 12 19:44 . + drwxr-xr-x 3 root root 4096 Oct 12 19:44 .. + drwxr-xr-x 2 1000 1000 4096 Oct 12 19:45 completed + drwxr-xr-x 2 1000 1000 4096 Oct 12 19:45 intermediate + +===== 7. memory at rest, both ===== +sabnzbd 68.66MiB / 1GiB 6.71% +nzbget 5.535MiB / 512MiB 1.08% + sabnzbd anon/peak/oom: anon 68640768 76898304 oom_kill 0 + nzbget anon/peak/oom: anon 6561792 11513856 oom_kill 0 diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/abort-states.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/abort-states.json new file mode 100644 index 00000000..f16b17c9 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/abort-states.json @@ -0,0 +1,8 @@ +{ + "nzbget": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/compose-final.log b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/compose-final.log new file mode 100644 index 00000000..8767d3b8 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/compose-final.log @@ -0,0 +1,5 @@ +nzbget | [ERROR] No servers are configured +nzbget | [WARNING] [Logging][WriteLog]: 'WriteLog' is set to 'Append'. The log file may grow indefinitely +nzbget | [INFO] nzbget 26.2 server-mode +nzbget | [INFO] using /config/nzbget.conf +nzbget | [INFO] nzbget runs on 0.0.0.0:6789 diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/engine-state.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/engine-state.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/engine-state.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-after-detail.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-after-detail.json new file mode 100644 index 00000000..868404b4 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-after-detail.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/nzbget": {} +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-after.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-after.json new file mode 100644 index 00000000..87e1d624 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-after.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/nzbget": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-before-detail.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-before-detail.json new file mode 100644 index 00000000..868404b4 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-before-detail.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/nzbget": {} +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-before.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-before.json new file mode 100644 index 00000000..87e1d624 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/files-before.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/nzbget": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/memory-samples.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/memory-samples.json new file mode 100644 index 00000000..01661c16 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/memory-samples.json @@ -0,0 +1,534 @@ +[ + { + "t": 15.1, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6578176, + "peak": 8122368, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 304 + }, + { + "t": 30.3, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6393856, + "peak": 8134656, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 604 + }, + { + "t": 45.4, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6066176, + "peak": 8134656, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 900 + }, + { + "t": 60.5, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6012928, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1204 + }, + { + "t": 75.6, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6111232, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1504 + }, + { + "t": 90.7, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6500352, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1804 + }, + { + "t": 105.9, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6340608, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2104 + }, + { + "t": 121.0, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6287360, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2404 + }, + { + "t": 136.1, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6029312, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2704 + }, + { + "t": 151.2, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5832704, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3004 + }, + { + "t": 166.4, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6316032, + "peak": 8777728, + "anon": 4861952, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3304 + }, + { + "t": 181.5, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6062080, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3604 + }, + { + "t": 196.7, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5935104, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3904 + }, + { + "t": 211.8, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6189056, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4204 + }, + { + "t": 226.9, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5767168, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4504 + }, + { + "t": 242.0, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6164480, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4804 + }, + { + "t": 257.1, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5890048, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5104 + }, + { + "t": 272.3, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5750784, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5404 + }, + { + "t": 287.4, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6135808, + "peak": 8859648, + "anon": 4866048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5704 + }, + { + "t": 302.5, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6045696, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6004 + }, + { + "t": 317.6, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5918720, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6304 + }, + { + "t": 332.8, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5943296, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6604 + }, + { + "t": 347.9, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6389760, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6904 + }, + { + "t": 363.0, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5865472, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7204 + }, + { + "t": 378.2, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5935104, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7504 + }, + { + "t": 393.3, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5607424, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7804 + }, + { + "t": 408.4, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 5980160, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8104 + }, + { + "t": 423.6, + "containers": { + "nzbget": { + "limit": 536870912, + "current": 6004736, + "peak": 8859648, + "anon": 4870144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8404 + } +] \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/migration-lines.txt b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/migration-lines.txt new file mode 100644 index 00000000..e69de29b diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/run.log b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/run.log new file mode 100644 index 00000000..292d7608 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/run.log @@ -0,0 +1,45 @@ +[20:10:11] scratch drive folders cleared before FROM (R-656): ['/srv/felhom-gate/hdd/userdata/media/nzbget/completed', '/srv/felhom-gate/hdd/userdata/media/nzbget'] +[20:10:11] MV-nzbget: deploying nzbget at FROM {'nzbget': 'nzbgetcom/nzbget:v26.2'} +[20:10:42] FROM settled=True in 30.9s :: {"nzbget": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[20:10:42] nzbget: append felhomc52299d4 http=200 ok=True +[20:10:43] nzbget: the queued job is present, its saved nzb file present=True +[20:10:43] C1 (seed reads back BEFORE): True +[20:10:43] MV-nzbget: swapping to TO {'nzbget': 'nzbgetcom/nzbget:v26.3'} +[20:10:44] TO up -d rc=0 +[20:11:15] TO settled=True in 30.9s :: {"nzbget": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[20:11:15] migration lines observed: 0 +[20:11:15] nzbget: the queued job is present, its saved nzb file present=True +[20:11:15] RESULT (seed reads back AFTER): True +[20:11:16] memory watch: 420s, 4 callers on 1 path(s) at 172.18.0.2:6789 +[20:11:31] + 15s nzbget=6M/512M peak=7M kills=0 rs=0 reqs=304 +[20:11:46] + 30s nzbget=6M/512M peak=7M kills=0 rs=0 reqs=604 +[20:12:01] + 45s nzbget=5M/512M peak=7M kills=0 rs=0 reqs=900 +[20:12:16] + 60s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=1204 +[20:12:31] + 76s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=1504 +[20:12:46] + 91s nzbget=6M/512M peak=8M kills=0 rs=0 reqs=1804 +[20:13:01] + 106s nzbget=6M/512M peak=8M kills=0 rs=0 reqs=2104 +[20:13:17] + 121s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=2404 +[20:13:32] + 136s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=2704 +[20:13:47] + 151s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=3004 +[20:14:02] + 166s nzbget=6M/512M peak=8M kills=0 rs=0 reqs=3304 +[20:14:17] + 182s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=3604 +[20:14:32] + 197s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=3904 +[20:14:47] + 212s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=4204 +[20:15:03] + 227s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=4504 +[20:15:18] + 242s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=4804 +[20:15:33] + 257s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=5104 +[20:15:48] + 272s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=5404 +[20:16:03] + 287s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=5704 +[20:16:18] + 302s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=6004 +[20:16:33] + 318s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=6304 +[20:16:48] + 333s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=6604 +[20:17:04] + 348s nzbget=6M/512M peak=8M kills=0 rs=0 reqs=6904 +[20:17:19] + 363s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=7204 +[20:17:34] + 378s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=7504 +[20:17:49] + 393s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=7804 +[20:18:04] + 408s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=8104 +[20:18:19] + 424s nzbget=5M/512M peak=8M kills=0 rs=0 reqs=8404 +[20:18:19] memory watch: killed=False tight=[] requests=8404 codes={'401': 8404} +[20:18:19] MV-nzbget: ABORT — putting the FROM images back +[20:18:52] nzbget: the queued job is present, its saved nzb file present=True +[20:18:52] ABORT: app came back in 30.9s; data present=True \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/to-full.log b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/to-full.log new file mode 100644 index 00000000..aa80f634 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/to-full.log @@ -0,0 +1,5 @@ +nzbget | [ERROR] No servers are configured +nzbget | [WARNING] [Logging][WriteLog]: 'WriteLog' is set to 'Append'. The log file may grow indefinitely +nzbget | [INFO] nzbget 26.3 server-mode +nzbget | [INFO] using /config/nzbget.conf +nzbget | [INFO] nzbget runs on 0.0.0.0:6789 diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/to-states.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/to-states.json new file mode 100644 index 00000000..f16b17c9 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/to-states.json @@ -0,0 +1,8 @@ +{ + "nzbget": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/verdict.json b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/verdict.json new file mode 100644 index 00000000..ad3e86ac --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/nzbget-bench/MV-nzbget/verdict.json @@ -0,0 +1,59 @@ +{ + "harness_version": 5, + "edge": "MV-nzbget", + "app": "nzbget", + "note": "night 2026-09-23 within-a-major move: nzbget=nzbgetcom/nzbget:v26.3", + "from": { + "nzbget": "nzbgetcom/nzbget:v26.2" + }, + "to": { + "nzbget": "nzbgetcom/nzbget:v26.3" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "migration_observed": null, + "abort": "starts-and-serves", + "abort_detail": null, + "engine_state_after": null, + "memory": { + "soak_s": 423.7, + "requested_s": 420, + "requests": 8404, + "codes": { + "401": 8404 + }, + "first_kill": null, + "containers": { + "nzbget": { + "limit": 536870912, + "peak": 8859648, + "peak_pct": 0.017, + "anon_peak_sampled": 4870144, + "anon_peak_pct": 0.009, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + } + }, + "unmeasured": [], + "venue_swap_bytes": 0, + "load": "reached" + }, + "marks": [], + "bench_overrides": null, + "duration_s": 30.9, + "measured_at": "2026-10-12T20:18:52Z", + "evidence": "evidence/MV-nzbget", + "scratch_cleared": [ + "/srv/felhom-gate/hdd/userdata/media/nzbget/completed", + "/srv/felhom-gate/hdd/userdata/media/nzbget" + ], + "files_changed": [], + "files_changed_detail": [], + "files_ignored": [], + "total_s": 520.7 +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/01-image.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/01-image.txt new file mode 100644 index 00000000..94c9041b --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/01-image.txt @@ -0,0 +1,8 @@ +# the image as pulled on the bench, 2026-10-12T19:33:15+0000 +lscr.io/linuxserver/sabnzbd:version-5.1.3 + +arch/os/size/created: amd64 linux 183424450 2026-10-08T06:15:00.293365309Z +platforms: linux/amd64, linux/arm64 +digest count: 4 + +repo digest: lscr.io/linuxserver/sabnzbd@sha256:382f9bd4e3f6df2abf7bcc92edada0c5bd4d4ff26eb5416f257ac54989464fa1 diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/02-telemetry-and-net.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/02-telemetry-and-net.txt new file mode 100644 index 00000000..7807e85c --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/02-telemetry-and-net.txt @@ -0,0 +1,12 @@ +# does it call home, and does it check for updates? +## the update-check setting as the app has it (check_new_rel; 0 = off): +check_new_rel = 0 +## what the app was told about its own update check (its API): +{"config":{"misc":{"config_conversion_version":5,"helpful_warnings":1,"queue_complete":"","queue_complete_pers":0,"bandwidth_perc":100,"refresh_rate":0,"interface_settings":"","queue_limit":20,"config_lock":0,"notified_new_skin":0,"check_new_rel":0,"auto_browser":false,"language":"en","enable_https_verification":1,"host":"::","port":"8080","https_port":"","username":"admin","password":"**********","bandwidth_max":"","cache_limit":"256.0 M","web_dir":"Glitter","web_color":"Auto","https_cert":"server.cert","https_key":"server.key","https_chain":"","enable_https":0,"inet_exposure":5,"api_key":"5d3aa684c50b0f13893eebf2e470fb4d","nzb_key":"b929cd0d49854401b92046d7afe28dff","socks5_proxy_url":"","permissions":"","download_dir":"/incomplete-downloads","download_free":"500M","complete_dir":"/downloads","complete_free":"","fulldisk_autoresume":0,"script_dir":"","nzb_backup_dir":"","admin_dir":"admin","backup_dir":"","dirscan_dir":"","dirscan_speed":5,"password_file":"","log_dir":"logs","max_art_tries":3,"top_only":0,"sfv_check":1,"script_can_fail":0,"enable_recursive":1,"flat_unpack":0,"par_option":"","pre_check":0,"nice":"","win_process_prio":3,"ionice":"","fail_hopeless_jobs":1,"fast_fail +## established outbound connections right now (nothing but the probe is expected): +Active Internet connections (w/o servers) +Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name +tcp 0 0 127.0.0.1:32946 127.0.0.1:8080 TIME_WAIT - +tcp 0 0 127.0.0.1:32952 127.0.0.1:8080 TIME_WAIT - +tcp 0 0 ::1:42438 ::1:8080 TIME_WAIT - +tcp 0 0 ::1:42440 ::1:8080 TIME_WAIT - diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/03-process-and-entrypoint.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/03-process-and-entrypoint.txt new file mode 100644 index 00000000..eaced7f5 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/03-process-and-entrypoint.txt @@ -0,0 +1,78 @@ +# what runs inside, as whom, and the entrypoint read in full +## processes: + PID USER COMMAND + 1 root /package/admin/s6/command/s6-svscan -d4 -- /run/service + 19 root s6-supervise s6-linux-init-shutdownd + 21 root /package/admin/s6-linux-init/command/s6-linux-init-shutdownd -d3 -c /run/s6/basedir -g 3000 -C -B + 38 root s6-supervise svc-cron + 39 root s6-supervise s6rc-oneshot-runner + 40 root s6-supervise s6rc-fdholder + 41 root s6-supervise svc-sabnzbd + 49 root /package/admin/s6/command/s6-ipcserverd -1 -- /package/admin/s6/command/s6-ipcserver-access -v0 -E -l0 -i data/rules -- /package/admin/s6/command/s6-sudod -t 30000 -- /package/admin/s6-rc/command/s6-rc-oneshot-run -l ../.. -- + 187 root busybox crond -f -S -l 5 + 308 root sh -c ps -o pid,user,args 2>/dev/null || ps aux + 314 root ps -o pid,user,args + +## the compose entrypoint this template sets: +["/bin/sh","-c","CONF=/config/sabnzbd.ini\nif [ ! -f \"$CONF\" ]; then\n mkdir -p /config\n {\n printf '%s\\n' '[misc]'\n printf '%s\\n' 'host = 0.0.0.0'\n printf '%s\\n' 'port = 8080'\n printf '%s\\n' 'username = admin'\n printf 'password = %s\\n' \"$SAB_ADMIN_PASSWORD\"\n printf 'api_key = %s\\n' \"$SAB_API_KEY\"\n printf '%s\\n' 'inet_exposure = 5'\n printf 'host_whitelist = %s, sabnzbd, localhost, 127.0.0.1\\n' \"$SAB_PUBLIC_HOST\"\n printf '%s\\n' 'download_dir = /incomplete-downloads'\n printf '%s\\n' 'complete_dir = /downloads'\n printf '%s\\n' 'check_new_rel = 0'\n printf '%s\\n' 'auto_browser = 0'\n } > \"$CONF\"\n chmod 600 \"$CONF\"\nfi\nexec /init\n"] +null + +## the image's own init, as the image carries it: +ci-service-check +init-adduser +init-config +init-config-end +init-crontab-config +init-custom-files +init-device-perms +init-envfile +init-migrations +init-mods +init-mods-end +init-mods-package-install +init-os-end +init-sabnzbd-config +init-services +svc-cron +svc-sabnzbd +user +user2 +--- +#!/usr/bin/with-contenv bash +# shellcheck shell=bash + +if [[ -z ${LSIO_NON_ROOT_USER} ]]; then + if grep -qe ' /downloads ' /proc/mounts; then + lsiown abc:abc /downloads + fi + + if grep -qe ' /incomplete-downloads ' /proc/mounts; then + lsiown abc:abc /incomplete-downloads + fi + + find /config -path /config/Downloads -prune -o -exec lsiown abc:abc {} + +fi +--- +#!/usr/bin/with-contenv bash +# shellcheck shell=bash + +FAMILY=:: + +if [[ -z ${LSIO_NON_ROOT_USER} ]]; then + if ! test -f /proc/net/if_inet6; then + FAMILY=0.0.0.0 + sed '/ip6-/d' /etc/hosts > /etc/hosts.new + cat /etc/hosts.new > /etc/hosts + rm /etc/hosts.new + fi + + exec \ + s6-notifyoncheck -d -n 300 -w 1000 \ + s6-setuidgid abc python3 /app/sabnzbd/SABnzbd.py \ + --config-file /config --server "$FAMILY" +else + exec \ + s6-notifyoncheck -d -n 300 -w 1000 \ + python3 /app/sabnzbd/SABnzbd.py \ + --config-file /config --server "$FAMILY" +fi diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/04-first-admin.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/04-first-admin.txt new file mode 100644 index 00000000..c9d676ca --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/04-first-admin.txt @@ -0,0 +1,24 @@ +# first admin: class 1. Measured 2026-10-12T19:33:15+0000 +## 1. the app's whole log, searched for a password it printed itself (want 0): +0 +## 2. an unauthenticated request (the app must not simply answer): +303 +## 3. the generated password is accepted: +303 +## 4. a wrong password is refused: +303 +## 5. the API without a key, and with the generated key: +no key: 403 +the key: 403 +## 6. the exposure level the app actually has (5 = external needs a login): +username = admin +inet_exposure = 5 +host_whitelist = usenet.bench.local, sabnzbd, localhost, 127.0.0.1 +## 7. six wrong tries in a row, then the right password from the same address: +try 1: 303 +try 2: 303 +try 3: 303 +try 4: 303 +try 5: 303 +try 6: 303 +right password right after: 303 diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/05-public-name.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/05-public-name.txt new file mode 100644 index 00000000..09d50c05 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/05-public-name.txt @@ -0,0 +1,9 @@ +# the Host header: what the app admits and what it refuses +Host: usenet.bench.local -> 303 +Host: sabnzbd -> 303 +Host: localhost -> 303 +Host: 127.0.0.1 -> 303 +Host: evil.example.com -> 303 + +## and with no Host override at all (the container's own healthcheck shape): +no Host header -> 303 diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/06-storage.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/06-storage.txt new file mode 100644 index 00000000..f6f0411c --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/06-storage.txt @@ -0,0 +1,84 @@ +# where the app keeps what, and what the template mounts +## the mounts this container has: +bind /opt/sabnzbd-bench/userdata/media/usenet/incomplete -> /incomplete-downloads +bind /opt/sabnzbd-bench/userdata/media/usenet/complete -> /downloads +volume /var/lib/docker/volumes/sabnzbd-bench_sabnzbd_config/_data -> /config + +## the config volume's content: +total 28 +drwxr-xr-x 4 abc users 4096 Oct 12 21:33 . +drwxr-xr-x 1 root root 4096 Oct 12 21:33 .. +drwxr-xr-x 3 abc users 4096 Oct 12 21:33 admin +drwxr-xr-x 2 abc users 4096 Oct 12 21:33 logs +-rw------- 1 abc users 7498 Oct 12 21:33 sabnzbd.ini +-rw------- 1 abc users 313 Oct 12 21:33 sabnzbd.ini.bak +## what the app wrote OUTSIDE its mounts (docker diff, A = added): +A /downloads +A /incomplete-downloads +A /run/crond.pid +A /run/crond.reboot +A /run/s6 +A /run/s6/legacy-services +A /run/s6/workdir +A /run/s6/basedir +A /run/s6/basedir/bin +A /run/s6/basedir/bin/halt +A /run/s6/basedir/bin/init +A /run/s6/basedir/bin/poweroff +A /run/s6/basedir/bin/reboot +A /run/s6/basedir/bin/shutdown +A /run/s6/basedir/env +A /run/s6/basedir/run-image +A /run/s6/basedir/run-image/s6-linux-init-container-results +A /run/s6/basedir/run-image/s6-linux-init-container-results/exitcode +A /run/s6/basedir/run-image/service +A /run/s6/basedir/run-image/service/.s6-svscan +A /run/s6/basedir/run-image/service/.s6-svscan/SIGINT +A /run/s6/basedir/run-image/service/.s6-svscan/SIGUSR2 +A /run/s6/basedir/run-image/service/.s6-svscan/finish +A /run/s6/basedir/run-image/service/.s6-svscan/SIGPWR +A /run/s6/basedir/run-image/service/.s6-svscan/SIGQUIT +A /run/s6/basedir/run-image/service/.s6-svscan/SIGTERM +A /run/s6/basedir/run-image/service/.s6-svscan/SIGUSR1 +A /run/s6/basedir/run-image/service/.s6-svscan/SIGWINCH +A /run/s6/basedir/run-image/service/.s6-svscan/crash +A /run/s6/basedir/run-image/service/s6-linux-init-shutdownd +A /run/s6/basedir/run-image/service/s6-linux-init-shutdownd/fifo +A /run/s6/basedir/run-image/service/s6-linux-init-shutdownd/notification-fd +A /run/s6/basedir/run-image/service/s6-linux-init-shutdownd/run +A /run/s6/basedir/scripts +A /run/s6/basedir/scripts/rc.init +A /run/s6/basedir/scripts/rc.shutdown +A /run/s6/basedir/scripts/rc.shutdown.final +A /run/s6/container_environment +A /run/s6/container_environment/SAB_ADMIN_PASSWORD +A /run/s6/container_environment/PWD +## the drive side: +/opt/sabnzbd-bench/userdata: +total 12 +drwxr-xr-x 3 root root 4096 Oct 12 19:33 . +drwxr-xr-x 4 root root 4096 Oct 12 19:32 .. +drwxr-xr-x 3 root root 4096 Oct 12 19:33 media + +/opt/sabnzbd-bench/userdata/media: +total 12 +drwxr-xr-x 3 root root 4096 Oct 12 19:33 . +drwxr-xr-x 3 root root 4096 Oct 12 19:33 .. +drwxr-xr-x 4 root root 4096 Oct 12 19:33 usenet + +/opt/sabnzbd-bench/userdata/media/usenet: +total 16 +drwxr-xr-x 4 root root 4096 Oct 12 19:33 . +drwxr-xr-x 3 root root 4096 Oct 12 19:33 .. +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:33 complete +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:33 incomplete + +/opt/sabnzbd-bench/userdata/media/usenet/complete: +total 8 +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:33 . +drwxr-xr-x 4 root root 4096 Oct 12 19:33 .. + +/opt/sabnzbd-bench/userdata/media/usenet/incomplete: +total 8 +drwxr-xr-x 2 1000 1000 4096 Oct 12 19:33 . +drwxr-xr-x 4 root root 4096 Oct 12 19:33 .. diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/07-ownership.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/07-ownership.txt new file mode 100644 index 00000000..07c1d40e --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/07-ownership.txt @@ -0,0 +1,14 @@ +# who owns the files the household will see +uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) +--- +abc users 755 /config +abc users 755 /config/admin +abc users 755 /config/logs +abc users 600 /config/sabnzbd.ini +abc users 600 /config/sabnzbd.ini.bak +## on the drive, from the host's side: +root root 755 /opt/sabnzbd-bench/userdata +root root 755 /opt/sabnzbd-bench/userdata/media +root root 755 /opt/sabnzbd-bench/userdata/media/usenet +1000 1000 755 /opt/sabnzbd-bench/userdata/media/usenet/complete +1000 1000 755 /opt/sabnzbd-bench/userdata/media/usenet/incomplete diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/08-health.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/08-health.txt new file mode 100644 index 00000000..25d69665 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/08-health.txt @@ -0,0 +1,9 @@ +# the healthcheck, and the same probe against a port nothing serves (the control) +{"Status":"starting","FailingStreak":0,"Log":[]} + +## the probe's own command: +["CMD-SHELL","curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/ | grep -qE '^(200|401)$'"] +## NEGATIVE CONTROL - the same shape against port 9 (discard), must fail: +rc=1 +## and the real one, run by hand: +rc=1 diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/09-seed-no-provider.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/09-seed-no-provider.txt new file mode 100644 index 00000000..2f099544 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/09-seed-no-provider.txt @@ -0,0 +1,14 @@ +# THE SEED: an NZB through the app's OWN interface, with no Usenet provider configured. +# A provider is a paid subscription the household brings; without one the articles cannot be +# fetched, so what is measured here is the QUEUE taking the job and keeping it - the store an +# upgrade must not lose. The download chain itself is NOT measured and the record says so. +## add it (mode=addfile) and then ask the queue: +{"status":true,"nzo_ids":["794e979c-ab62-4e97-bffa-3b34faac4f2c"]} +## a category the household would make (the config store): +{"config":{"categories":[{"name":"felhom","order":0,"pp":"","script":"Default","dir":"felhom","newzbin":"","priority":-100}]}} +## the queue, read back: +{"queue":{"version":"5.1.3","paused":false,"pause_int":"0","paused_all":false,"diskspace1":"48.24","diskspace2":"48.24","diskspace1_norm":"48.2 G","diskspace2_norm":"48.2 G","diskspacetotal1":"58.76","diskspacetotal2":"58.76","speedlimit":"0","speedlimit_abs":"0","have_warnings":"2","finishaction":null,"quota":"0 B","have_quota":false,"left_quota":"0 B","cache_art":"0","cache_size":"0 B","kbpersec":"0.00","speed":"0","mbleft":"0.00","mb":"0.00","sizeleft":"1 KB","size":"1 KB","noofslots_total":1,"noofslots":1,"start":0,"limit":0,"finish":0,"status":"Idle","timeleft":"0:00:00","slots":[{"index":0,"nzo_id":"794e979c-ab62-4e97-bffa-3b34faac4f2c","unpackopts":"3","priority":"Normal","script":"None","filename":"felhom-fixture","labels":[],"password":"","cat":"*","mbleft":"0.00","mb":"0.00","size":"1 KB","sizeleft":"1 KB","percentage":"0","mbmissing":"0.00","direct_unpack":null,"status":"Downloading","timeleft":"0:00:00","avg_age":"368d","time_added":1791833598}]}} +## the history (a job that cannot be fetched lands here): +{"history":{"total_size":"0","month_size":"0","week_size":"0","day_size":"0","slots":[],"ppslots":0,"noofslots":0,"last_history_update":1,"version":"5.1.3"}} +## NEGATIVE CONTROL - a category nobody created must be absent: +{"config":{"categories":[{"name":"*","order":0,"pp":"3","script":"None","dir":"","newzbin":"","priority":0},{"name":"felhom","order":0,"pp":"","script":"Default","dir":"felhom","newzbin":"","priority":-100},{"name":"movies","order":1,"pp":"","script":"Default","dir":"","newzbin":"","priority":-100},{"name":"tv","order":2,"pp":"","script":"Default","dir":"","newzbin":"","priority":-100},{"name":"audio","order":3,"pp":"","script":"Default","dir":"","newzbin":"","priority":-100},{"name":"software","order":4,"pp":"","script":"Default","dir":"","newzbin":"","priority":-100}]}} diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/10-config-as-written.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/10-config-as-written.txt new file mode 100644 index 00000000..b7282ddd --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/10-config-as-written.txt @@ -0,0 +1,195 @@ +# the config file as the APP has it now - a seeded value is only a request (R-qbt 2026-10-12) +__encoding__ = utf-8 +__version__ = 19 +[misc] +host = :: +port = 8080 +username = admin +password = <redacted> +api_key = <redacted> +inet_exposure = 5 +host_whitelist = usenet.bench.local, sabnzbd, localhost, 127.0.0.1 +download_dir = /incomplete-downloads +complete_dir = /downloads +check_new_rel = 0 +auto_browser = 0 +config_conversion_version = 5 +helpful_warnings = 1 +queue_complete = "" +queue_complete_pers = 0 +bandwidth_perc = 100 +refresh_rate = 0 +interface_settings = "" +queue_limit = 20 +config_lock = 0 +fixed_ports = 1 +notified_new_skin = 0 +direct_unpack_tested = 0 +sorters_converted = 1 +language = en +enable_https_verification = 1 +https_port = "" +bandwidth_max = "" +cache_limit = 256.0 M +web_dir = Glitter +web_color = Auto +https_cert = server.cert +https_key = server.key +https_chain = "" +enable_https = 0 +nzb_key = <redacted> +socks5_proxy_url = "" +permissions = "" +download_free = 500M +complete_free = "" +fulldisk_autoresume = 0 +script_dir = "" +nzb_backup_dir = "" +admin_dir = admin +backup_dir = "" +dirscan_dir = "" +dirscan_speed = 5 +password_file = "" +log_dir = logs +max_art_tries = 3 +top_only = 0 +sfv_check = 1 +script_can_fail = 0 +enable_recursive = 1 +flat_unpack = 0 +par_option = "" +pre_check = 0 +nice = "" +win_process_prio = 3 +ionice = "" +fail_hopeless_jobs = 1 +fast_fail = 1 +auto_disconnect = 1 +pre_script = None +end_queue_script = None +no_dupes = 0 +no_series_dupes = 0 +no_smart_dupes = 0 +dupes_propercheck = 1 +pause_on_pwrar = 1 +ignore_samples = 0 +deobfuscate_final_filenames = 1 +auto_sort = "" +direct_unpack = 0 +propagation_delay = 0 +folder_rename = 1 +replace_spaces = 0 +replace_underscores = 0 +replace_dots = 0 +safe_postproc = 1 +pause_on_post_processing = 0 +enable_all_par = 0 +sanitize_safe = 0 +cleanup_list = , +unwanted_extensions = , +action_on_unwanted_extensions = 0 +unwanted_extensions_mode = 0 +new_nzb_on_failure = 0 +history_retention = "" +history_retention_option = all +history_retention_number = 0 +quota_size = "" +quota_day = "" +quota_resume = 0 +quota_period = m +enable_tv_sorting = 0 +tv_sort_string = "" +tv_categories = tv, +enable_movie_sorting = 0 +movie_sort_string = "" +movie_sort_extra = -cd%1 +movie_categories = movies, +enable_date_sorting = 0 +date_sort_string = "" +date_categories = tv, +schedlines = , +rss_rate = 60 +ampm = 0 +start_paused = 0 +preserve_paused_state = 0 +enable_par_cleanup = 1 +process_unpacked_par2 = 1 +enable_unrar = 1 +enable_7zip = 1 +enable_filejoin = 1 +enable_tsjoin = 1 +enable_tar = 1 +overwrite_files = 0 +ignore_unrar_dates = 0 +backup_for_duplicates = 0 +wait_for_dfolder = 0 +rss_filenames = 0 +api_logging = 1 +html_login = 1 +disable_archive = 0 +warn_dupl_jobs = 0 +keep_awake = 1 +tray_icon = 1 +allow_incomplete_nzb = 0 +enable_broadcast = 1 +ipv6_hosting = 0 +ipv6_staging = 0 +api_warnings = 1 +no_penalties = 0 +x_frame_options = 1 +allow_old_ssl_tls = 0 +enable_season_sorting = 1 +verify_xff_header = 1 +direct_write = 1 +rss_odd_titles = nzbindex.nl/, nzbindex.com/, nzbclub.com/ +quick_check_ext_ignore = nfo, sfv, srr +req_completion_rate = 100.2 +selftest_host = self-test.sabnzbd.org +movie_rename_limit = 100M +episode_rename_limit = 20M +size_limit = 0 +direct_unpack_threads = 3 +history_limit = 10 +wait_ext_drive = 5 +max_foldername_length = 246 +nomedia_marker = "" +ipv6_servers = 1 +url_base = "" +local_ranges = , +max_url_retries = 10 +downloader_sleep_time = 10 +receive_threads = 2 +assembler_max_queue_size = 12 +switchinterval = 0.005 +ssdp_broadcast_interval = 15 +ext_rename_ignore = , +unrar_parameters = "" +outgoing_nntp_ip = "" +email_server = "" +email_to = , +email_from = "" +email_account = "" +email_pwd = "" +email_endjob = 0 +email_full = 0 +email_dir = "" +email_rss = 0 +email_cats = *, +[logging] +log_level = 1 +max_log_size = 5242880 +log_backups = 5 +[ncenter] +ncenter_enable = 0 +ncenter_cats = *, +ncenter_prio_startup = 0 +ncenter_prio_download = 0 +ncenter_prio_pause_resume = 0 +ncenter_prio_pp = 0 +ncenter_prio_complete = 1 +ncenter_prio_failed = 1 +ncenter_prio_disk_full = 1 +ncenter_prio_quota = 1 +ncenter_prio_new_login = 0 +ncenter_prio_warning = 0 +ncenter_prio_error = diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/11-memory.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/11-memory.txt new file mode 100644 index 00000000..93b180b3 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/11-memory.txt @@ -0,0 +1,15 @@ +# memory at rest, from birth (anon is the judge - the page cache is not the app's, R-harness) +anon 67145728 +file 1232896 +76890112 +low 0 +high 0 +max 0 +oom 0 +oom_kill 0 +oom_group_kill 0 +sock_throttled 0 +## docker's own view: +sabnzbd 68.91MiB / 1GiB 6.73% +## swap on the bench (0 means a kill cannot hide as swapping): +Swap: 0 0 0 diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/12-container-log.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/12-container-log.txt new file mode 100644 index 00000000..9559f25f --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/12-container-log.txt @@ -0,0 +1,60 @@ +2026-10-12 21:33:06,829::INFO::[cfg:878] Config conversion set 4 +2026-10-12 21:33:06,829::INFO::[cfg:890] Config conversion set 5 +2026-10-12 21:33:06,830::INFO::[config:995] Writing settings to INI file /config/sabnzbd.ini +2026-10-12 21:33:06,832::INFO::[filesystem:798] Creating directories: /config/admin/future +2026-10-12 21:33:06,832::INFO::[filesystem:1267] [N/A] /config/admin/totals10.sab missing +2026-10-12 21:33:06,916::INFO::[postproc:153] Loading postproc queue +2026-10-12 21:33:06,916::INFO::[filesystem:1267] [N/A] /config/admin/postproc2.sab missing +2026-10-12 21:33:06,916::INFO::[filesystem:1267] [N/A] /config/admin/watched_data2.sab missing +2026-10-12 21:33:06,917::INFO::[scheduler:191] Scheduling RSS interval task every 60 min (delay=22) +2026-10-12 21:33:06,917::INFO::[scheduler:215] Setting schedule for midnight auto history-purge +2026-10-12 21:33:06,917::INFO::[scheduler:224] Setting schedule for midnight BPS reset +2026-10-12 21:33:06,917::INFO::[scheduler:233] Setting schedule for midnight server expiration check +2026-10-12 21:33:06,917::INFO::[scheduler:242] Setting schedule for server quota check +2026-10-12 21:33:06,917::INFO::[filesystem:1267] [N/A] /config/admin/queue10.sab missing +2026-10-12 21:33:06,918::INFO::[__init__:315] All processes started +2026-10-12 21:33:06,918::INFO::[SABnzbd:333] Template location for Glitter is /app/sabnzbd/interfaces/Glitter +2026-10-12 21:33:06,918::INFO::[SABnzbd:333] Template location for Config is /app/sabnzbd/interfaces/Config +2026-10-12 21:33:06,919::INFO::[misc:1469] [N/A] Running external command: ['/usr/bin/unrar'] +2026-10-12 21:33:06,922::INFO::[misc:1469] [N/A] Running external command: ['/usr/bin/7zz'] +2026-10-12 21:33:06,925::INFO::[misc:1469] [N/A] Running external command: ['/usr/local/bin/par2', '-V'] +2026-10-12 21:33:06,928::INFO::[SABnzbd:418] SABCTools module (v9.6.3)... found! +2026-10-12 21:33:06,928::INFO::[SABnzbd:419] SABCTools module is using SIMD set: AVX2 +2026-10-12 21:33:06,928::INFO::[SABnzbd:420] SABCTools module is linked to OpenSSL: True +2026-10-12 21:33:06,929::INFO::[SABnzbd:437] Cryptography module (v50.0.2)... found! +2026-10-12 21:33:06,929::INFO::[SABnzbd:440] par2 binary... found (/usr/local/bin/par2) +2026-10-12 21:33:06,929::INFO::[SABnzbd:447] UNRAR binary... found (/usr/bin/unrar) +2026-10-12 21:33:06,929::INFO::[SABnzbd:455] UNRAR binary version 7.23 +2026-10-12 21:33:06,929::INFO::[SABnzbd:462] 7za binary... found (/usr/bin/7zz) +2026-10-12 21:33:06,929::INFO::[SABnzbd:464] 7za binary version 26.01 +2026-10-12 21:33:06,929::INFO::[SABnzbd:470] nice binary... found (/bin/nice) +2026-10-12 21:33:06,929::INFO::[SABnzbd:474] ionice binary... found (/bin/ionice) +2026-10-12 21:33:06,930::INFO::[SABnzbd:1351] Starting web-interface on :::8080 +2026-10-12 21:33:06,931::INFO::[_cplogging:213] [12/Oct/2026:21:33:06] ENGINE Bus STARTING +2026-10-12 21:33:07,034::INFO::[_cplogging:213] [12/Oct/2026:21:33:07] ENGINE Serving on http://:::8080 +2026-10-12 21:33:07,035::INFO::[_cplogging:213] [12/Oct/2026:21:33:07] ENGINE Bus STARTED +2026-10-12 21:33:07,035::INFO::[SABnzbd:1390] Starting SABnzbd.py-5.1.3 +2026-10-12 21:33:07,038::INFO::[dirscanner:121] Dirscanner starting up +2026-10-12 21:33:07,038::INFO::[notifier:169] Sending notification: SABnzbd - SABnzbd 5.1.3 started (type=startup, job_cat=None) +2026-10-12 21:33:07,038::INFO::[__init__:486] Commit = a647f73426cb70ad60ae7964b96c2d5e939b4727 +2026-10-12 21:33:07,039::INFO::[__init__:487] Python-version = 3.14.8 (main, Oct 1 2026, 11:52:26) [GCC 15.2.0] +2026-10-12 21:33:07,039::INFO::[__init__:488] CPU architecture = x86_64 +2026-10-12 21:33:07,039::INFO::[__init__:489] Platform = Docker +2026-10-12 21:33:07,039::INFO::[__init__:490] JSON-module = orjson 3.11.9 +2026-10-12 21:33:07,039::INFO::[__init__:491] Preferred encoding = UTF-8 +2026-10-12 21:33:07,039::INFO::[__init__:492] SSL version = OpenSSL 3.5.9 29 Sep 2026 +2026-10-12 21:33:07,045::INFO::[zconfig:61] No bonjour/zeroconf support installed +2026-10-12 21:33:07,046::INFO::[ssdp:109] Serving SSDP on 172.18.0.3 as SABnzbd +[ls.io-init] done. +2026-10-12 21:33:16,108::INFO::[notifier:169] Sending notification: Warning - API key missing, please enter the API key from Config->General into your 3rd party program: ::ffff:127.0.0.1 [curl/8.22.0] (type=warning, job_cat=None) +2026-10-12 21:33:16,108::WARNING::[interface:444] API key missing, please enter the API key from Config->General into your 3rd party program: ::ffff:127.0.0.1 [curl/8.22.0] +2026-10-12 21:33:16,185::WARNING::[interface:444] API key missing, please enter the API key from Config->General into your 3rd party program: ::ffff:127.0.0.1 [curl/8.22.0] +2026-10-12 21:33:18,379::INFO::[nzbparser:96] Attempting to add felhom-fixture.nzb +2026-10-12 21:33:18,380::INFO::[filesystem:798] Creating directories: /incomplete-downloads/felhom-fixture +2026-10-12 21:33:18,380::INFO::[filesystem:798] Creating directories: /incomplete-downloads/felhom-fixture/__ADMIN__ +2026-10-12 21:33:18,381::INFO::[filesystem:1346] Saving /incomplete-downloads/felhom-fixture/__ADMIN__/felhom-fixture.nzb.gz +2026-10-12 21:33:18,382::INFO::[object:590] File felhom-fixture.bin added to queue +2026-10-12 21:33:18,384::INFO::[config:995] Writing settings to INI file /config/sabnzbd.ini +2026-10-12 21:33:18,386::INFO::[nzbqueue:241] Saving queue +2026-10-12 21:33:18,387::INFO::[notifier:169] Sending notification: NZB added to queue - felhom-fixture.nzb (type=download, job_cat=*) +2026-10-12 21:33:18,480::INFO::[config:995] Writing settings to INI file /config/sabnzbd.ini diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/13-compose-resolved.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/13-compose-resolved.txt new file mode 100644 index 00000000..15c82990 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/13-compose-resolved.txt @@ -0,0 +1,81 @@ +name: sabnzbd-bench +services: + sabnzbd: + container_name: sabnzbd + deploy: + resources: + limits: + memory: "1073741824" + entrypoint: + - /bin/sh + - -c + - | + CONF=/config/sabnzbd.ini + if [ ! -f "$$CONF" ]; then + mkdir -p /config + { + printf '%s\n' '[misc]' + printf '%s\n' 'host = 0.0.0.0' + printf '%s\n' 'port = 8080' + printf '%s\n' 'username = admin' + printf 'password = %s\n' "$$SAB_ADMIN_PASSWORD" + printf 'api_key = %s\n' "$$SAB_API_KEY" + printf '%s\n' 'inet_exposure = 5' + printf 'host_whitelist = %s, sabnzbd, localhost, 127.0.0.1\n' "$$SAB_PUBLIC_HOST" + printf '%s\n' 'download_dir = /incomplete-downloads' + printf '%s\n' 'complete_dir = /downloads' + printf '%s\n' 'check_new_rel = 0' + printf '%s\n' 'auto_browser = 0' + } > "$$CONF" + chmod 600 "$$CONF" + fi + exec /init + environment: + PGID: "1000" + PUID: "1000" + SAB_ADMIN_PASSWORD: 85tZ4avOYyifUzc8NxqXHxZ3 + SAB_API_KEY: 5d3aa684c50b0f13893eebf2e470fb4d + SAB_PUBLIC_HOST: usenet.bench.local + TZ: Europe/Budapest + UMASK: "022" + healthcheck: + test: + - CMD-SHELL + - curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/ | grep -qE '^(200|401)$$' + timeout: 5s + interval: 30s + retries: 3 + start_period: 1m0s + image: lscr.io/linuxserver/sabnzbd:version-5.1.3 + labels: + traefik.enable: "true" + traefik.http.routers.sabnzbd.entrypoints: websecure + traefik.http.routers.sabnzbd.rule: Host(`usenet.bench.local`) + traefik.http.routers.sabnzbd.tls: "true" + traefik.http.routers.sabnzbd.tls.certresolver: letsencrypt + traefik.http.services.sabnzbd.loadbalancer.server.port: "8080" + networks: + traefik-public: null + restart: unless-stopped + volumes: + - type: volume + source: sabnzbd_config + target: /config + volume: {} + - type: bind + source: /opt/sabnzbd-bench/userdata/media/usenet/complete + target: /downloads + bind: + create_host_path: true + - type: bind + source: /opt/sabnzbd-bench/userdata/media/usenet/incomplete + target: /incomplete-downloads + bind: + create_host_path: true +networks: + traefik-public: + name: traefik-public + external: true +volumes: + sabnzbd_config: + name: sabnzbd-bench_sabnzbd_config diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/14-both-after-the-fixes.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/14-both-after-the-fixes.txt new file mode 100644 index 00000000..12237abe --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/14-both-after-the-fixes.txt @@ -0,0 +1,91 @@ +##################### sabnzbd: redeploy from the fixed template ##################### + Container sabnzbd Starting + Container sabnzbd Started +##################### nzbget: redeploy from the fixed template ##################### + Container nzbget Starting + Container nzbget Started + +waiting for both to settle... + +===== 1. docker's health verdict for both ===== + sabnzbd: healthy (streak 0) + nzbget: healthy (streak 0) + --- the probes by hand, with their negative controls --- + sabnzbd live rc=0 + sabnzbd dead rc=1 + nzbget live rc=0 + nzbget dead rc=1 +wget: can't connect to remote host (127.0.0.1): Connection refused + +===== 2. SABnzbd: is a stranger now asked to log in? (THE fix) ===== + no credentials, following redirects: 200 + what page that is: <title>SABnzbd - Log in + the generated password: 200 + what THAT page is: <title>SABnzbd - Log in + a wrong password: 200 + the API with the key (the *arr path): {"version":"5.1.3"} + the API with no key: {"version":"5.1.3"} + local_ranges, as the app has it: [local_ranges = 127.0.0.1,] + +===== 3. SABnzbd: the Host allow-list, now that nothing is local ===== + Host: usenet.bench.local -> {"version":"5.1.3"} + Host: sabnzbd -> {"version":"5.1.3"} + Host: localhost -> {"version":"5.1.3"} + Host: 127.0.0.1 -> {"version":"5.1.3"} + Host: evil.example.com -> {"version":"5.1.3"} + +===== 4. NZBGet: the file now carries the generated password, so its own CLI works ===== + env hash: 8fab4a9ffd808117 (length 24) + file hash: 8fab4a9ffd808117 (length 24) + SAME + Request sent + Server has no files queued for download + Current download rate: 0 KB/s + the well-known default in the file (want 0): 0 + the default login over HTTP (want 401): 401 + the generated login (want 200): 200 + +===== 5. both seeds, through each app's own API, with no provider ===== + sabnzbd addfile -> {"status":true,"nzo_ids":["0f176380-ab94-430e-abb7-5d24e10c5309"]} + nzbget append -> {"version" : "1.1","id" : 1,"result" : 1} + sabnzbd queue has the job: 1 + nzbget queue has the job: 4 + NEGATIVE CONTROLS (a job nobody added): sab=0 nzb=0 + +===== 6. where each app keeps its state, now ===== + sabnzbd /config: + admin + logs + sabnzbd.ini + sabnzbd.ini.bak + admin dir: + future + history1.db + queue10.sab + sabnzbd on the drive: + total 16 + drwxr-xr-x 4 root root 4096 Oct 12 19:44 . + drwxr-xr-x 3 root root 4096 Oct 12 19:44 .. + drwxr-xr-x 2 1000 1000 4096 Oct 12 19:44 complete + drwxr-xr-x 3 1000 1000 4096 Oct 12 19:45 incomplete + nzbget /config/data: + nzb + nzbget-2026-10-12.log + queue + scripts + tmp + 1 + n1.log + queue + nzbget on the drive: + total 16 + drwxr-xr-x 4 1000 1000 4096 Oct 12 19:44 . + drwxr-xr-x 3 root root 4096 Oct 12 19:44 .. + drwxr-xr-x 2 1000 1000 4096 Oct 12 19:45 completed + drwxr-xr-x 2 1000 1000 4096 Oct 12 19:45 intermediate + +===== 7. memory at rest, both ===== +sabnzbd 68.66MiB / 1GiB 6.71% +nzbget 5.535MiB / 512MiB 1.08% + sabnzbd anon/peak/oom: anon 68640768 76898304 oom_kill 0 + nzbget anon/peak/oom: anon 6561792 11513856 oom_kill 0 diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/15-sabnzbd-login.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/15-sabnzbd-login.txt new file mode 100644 index 00000000..c640e6f2 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/15-sabnzbd-login.txt @@ -0,0 +1,50 @@ + Container sabnzbd Starting + Container sabnzbd Started +===== what the app has now ===== +username = admin +inet_exposure = 5 +local_ranges = 127.0.0.1, +host_whitelist = usenet.bench.local, sabnzbd, localhost, 127.0.0.1 + +===== 1. A STRANGER (no credentials), through a 172.18.x address like traefik's ===== + GET / -> 303 then http://172.18.0.3:8080/login/ + following it: 200 + the page it lands on: <title>SABnzbd - Log in + can a stranger reach the wizard directly? 303 + the config page? 303 + +===== 2. THE HOUSEHOLD: a real sign-in (POST /login, keep the cookie) ===== + POST /login -> 303 then http://172.18.0.3:8080/ + then GET / with the cookie -> 303 + the page: <title>SABnzbd Quick-Start Wizard + the config page with the cookie -> 200 + +===== 3. A WRONG PASSWORD must not get a session ===== + POST /login (wrong) -> 200 + then GET / -> 303 + cookie file holds a session? 0 +0 + +===== 4. the *arr path: the API by container name, with and without the key ===== + with the key: {"queue":{"version":"5.1.3","paused":false,"pause_int":"0"," + no key: API Key Required + wrong key: API Key Incorrect + +===== 5. the healthcheck (loopback, so bypassed) and its negative control ===== + docker's verdict: healthy streak 0 + live rc=0 + dead rc=1 + +===== 6. six wrong sign-ins, then the household ===== + try 1: 200 + try 2: 200 + try 3: 200 + try 4: 200 + try 5: 200 + try 6: 200 + the household right after: 303 then GET / -> 303 + +===== 7. the seed still works through the API ===== + addfile -> {"status":true,"nzo_ids":["8d13e414-dd95-4ec3-91c2-79344886cd34"]} + queue has it: 1 (control, a job nobody added: 0) + the queue index in the volume: future history1.db queue10.sab diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/abort-states.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/abort-states.json new file mode 100644 index 00000000..1ba69f9f --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/abort-states.json @@ -0,0 +1,8 @@ +{ + "sabnzbd": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/compose-final.log b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/compose-final.log new file mode 100644 index 00000000..ae33248e --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/compose-final.log @@ -0,0 +1,77 @@ +sabnzbd | [migrations] started +sabnzbd | [migrations] no migrations found +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | ██╗ ███████╗██╗ ██████╗ +sabnzbd | ██║ ██╔════╝██║██╔═══██╗ +sabnzbd | ██║ ███████╗██║██║ ██║ +sabnzbd | ██║ ╚════██║██║██║ ██║ +sabnzbd | ███████╗███████║██║╚██████╔╝ +sabnzbd | ╚══════╝╚══════╝╚═╝ ╚═════╝ +sabnzbd | +sabnzbd | Brought to you by linuxserver.io +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | To support the app dev(s) visit: +sabnzbd | SABnzbd: https://sabnzbd.org/donate +sabnzbd | +sabnzbd | To support LSIO projects visit: +sabnzbd | https://www.linuxserver.io/donate/ +sabnzbd | +sabnzbd | ─────────────────────────────────────── +sabnzbd | GID/UID +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | User UID: 1000 +sabnzbd | User GID: 1000 +sabnzbd | ─────────────────────────────────────── +sabnzbd | Linuxserver.io version: 5.1.2-ls271 +sabnzbd | Build-date: 2026-09-03T04:24:31+00:00 +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | [custom-init] No custom files found, skipping... +sabnzbd | 2026-10-12 22:43:18,723::INFO::[SABnzbd:1135] -------------------------------- +sabnzbd | 2026-10-12 22:43:18,723::INFO::[SABnzbd:1136] SABnzbd.py-5.1.2 +sabnzbd | 2026-10-12 22:43:18,723::INFO::[SABnzbd:1137] Full executable path = /app/sabnzbd/SABnzbd.py +sabnzbd | 2026-10-12 22:43:18,723::INFO::[SABnzbd:1138] Arguments = "/app/sabnzbd/SABnzbd.py" "--config-file" "/config" "--server" "::" +sabnzbd | 2026-10-12 22:43:18,725::INFO::[SABnzbd:1147] Certifi version = 2026.06.17 +sabnzbd | 2026-10-12 22:43:18,725::INFO::[SABnzbd:1148] Loaded additional certificates from /lsiopy/lib/python3.14/site-packages/certifi/cacert.pem +sabnzbd | 2026-10-12 22:43:18,725::INFO::[SABnzbd:1154] Using INI file /config/sabnzbd.ini +sabnzbd | 2026-10-12 22:43:18,726::INFO::[postproc:153] Loading postproc queue +sabnzbd | 2026-10-12 22:43:18,727::INFO::[scheduler:191] Scheduling RSS interval task every 60 min (delay=34) +sabnzbd | 2026-10-12 22:43:18,727::INFO::[scheduler:215] Setting schedule for midnight auto history-purge +sabnzbd | 2026-10-12 22:43:18,727::INFO::[scheduler:224] Setting schedule for midnight BPS reset +sabnzbd | 2026-10-12 22:43:18,727::INFO::[scheduler:233] Setting schedule for midnight server expiration check +sabnzbd | 2026-10-12 22:43:18,727::INFO::[scheduler:242] Setting schedule for server quota check +sabnzbd | 2026-10-12 22:43:18,728::INFO::[__init__:315] All processes started +sabnzbd | 2026-10-12 22:43:18,728::INFO::[SABnzbd:333] Template location for Glitter is /app/sabnzbd/interfaces/Glitter +sabnzbd | 2026-10-12 22:43:18,728::INFO::[SABnzbd:333] Template location for Config is /app/sabnzbd/interfaces/Config +sabnzbd | 2026-10-12 22:43:18,729::INFO::[misc:1469] [N/A] Running external command: ['/usr/bin/unrar'] +sabnzbd | 2026-10-12 22:43:18,731::INFO::[misc:1469] [N/A] Running external command: ['/usr/bin/7zz'] +sabnzbd | 2026-10-12 22:43:18,735::INFO::[misc:1469] [N/A] Running external command: ['/usr/local/bin/par2', '-V'] +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:418] SABCTools module (v9.6.3)... found! +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:419] SABCTools module is using SIMD set: AVX2 +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:420] SABCTools module is linked to OpenSSL: True +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:437] Cryptography module (v50.0.1)... found! +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:440] par2 binary... found (/usr/local/bin/par2) +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:447] UNRAR binary... found (/usr/bin/unrar) +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:455] UNRAR binary version 7.23 +sabnzbd | 2026-10-12 22:43:18,739::INFO::[SABnzbd:462] 7za binary... found (/usr/bin/7zz) +sabnzbd | 2026-10-12 22:43:18,740::INFO::[SABnzbd:464] 7za binary version 26.01 +sabnzbd | 2026-10-12 22:43:18,740::INFO::[SABnzbd:470] nice binary... found (/bin/nice) +sabnzbd | 2026-10-12 22:43:18,740::INFO::[SABnzbd:474] ionice binary... found (/bin/ionice) +sabnzbd | 2026-10-12 22:43:18,740::INFO::[SABnzbd:1350] Starting web-interface on :::8080 +sabnzbd | 2026-10-12 22:43:18,741::INFO::[_cplogging:213] [12/Oct/2026:22:43:18] ENGINE Bus STARTING +sabnzbd | 2026-10-12 22:43:18,843::INFO::[_cplogging:213] [12/Oct/2026:22:43:18] ENGINE Serving on http://:::8080 +sabnzbd | 2026-10-12 22:43:18,844::INFO::[_cplogging:213] [12/Oct/2026:22:43:18] ENGINE Bus STARTED +sabnzbd | 2026-10-12 22:43:18,844::INFO::[SABnzbd:1389] Starting SABnzbd.py-5.1.2 +sabnzbd | 2026-10-12 22:43:18,846::INFO::[dirscanner:121] Dirscanner starting up +sabnzbd | 2026-10-12 22:43:18,846::INFO::[notifier:169] Sending notification: SABnzbd - SABnzbd 5.1.2 started (type=startup, job_cat=None) +sabnzbd | 2026-10-12 22:43:18,846::INFO::[__init__:486] Commit = 10609644c8b5eef462845ec6443f24dd8fcba96a +sabnzbd | 2026-10-12 22:43:18,847::INFO::[__init__:487] Python-version = 3.14.7 (main, Aug 15 2026, 07:18:27) [GCC 15.2.0] +sabnzbd | 2026-10-12 22:43:18,847::INFO::[__init__:488] CPU architecture = x86_64 +sabnzbd | 2026-10-12 22:43:18,847::INFO::[__init__:489] Platform = Docker +sabnzbd | 2026-10-12 22:43:18,847::INFO::[__init__:490] JSON-module = orjson 3.11.9 +sabnzbd | 2026-10-12 22:43:18,847::INFO::[__init__:491] Preferred encoding = UTF-8 +sabnzbd | 2026-10-12 22:43:18,847::INFO::[__init__:492] SSL version = OpenSSL 3.5.8 25 Aug 2026 +sabnzbd | [ls.io-init] done. diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/engine-state.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/engine-state.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/engine-state.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-after-detail.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-after-detail.json new file mode 100644 index 00000000..c6df16de --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-after-detail.json @@ -0,0 +1,8 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/usenet": { + "incomplete/felhom01ddfbf7/__ADMIN__/SABnzbd_nzf_ne33ep5w": "0:e3b0c44298fc1c14", + "incomplete/felhom01ddfbf7/__ADMIN__/SABnzbd_attrib": "111:21d7bd93572ec5dc", + "incomplete/felhom01ddfbf7/__ADMIN__/SABnzbd_nzo_data": "1728:41b438decc01dacc", + "incomplete/felhom01ddfbf7/__ADMIN__/felhom01ddfbf7.nzb.gz": "322:0542eee47e6ee818" + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-after.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-after.json new file mode 100644 index 00000000..9f6ba61c --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-after.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/usenet": "a7f5884d1f5206921249c885521c10ee307690d99281f89ac6b93ac07d30fe88" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-before-detail.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-before-detail.json new file mode 100644 index 00000000..c6df16de --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-before-detail.json @@ -0,0 +1,8 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/usenet": { + "incomplete/felhom01ddfbf7/__ADMIN__/SABnzbd_nzf_ne33ep5w": "0:e3b0c44298fc1c14", + "incomplete/felhom01ddfbf7/__ADMIN__/SABnzbd_attrib": "111:21d7bd93572ec5dc", + "incomplete/felhom01ddfbf7/__ADMIN__/SABnzbd_nzo_data": "1728:41b438decc01dacc", + "incomplete/felhom01ddfbf7/__ADMIN__/felhom01ddfbf7.nzb.gz": "322:0542eee47e6ee818" + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-before.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-before.json new file mode 100644 index 00000000..9f6ba61c --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/files-before.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media/usenet": "a7f5884d1f5206921249c885521c10ee307690d99281f89ac6b93ac07d30fe88" +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/memory-samples.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/memory-samples.json new file mode 100644 index 00000000..75e2cecd --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/memory-samples.json @@ -0,0 +1,382 @@ +[ + { + "t": 15.1, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 73420800, + "peak": 75882496, + "anon": 67096576, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 300 + }, + { + "t": 30.2, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74285056, + "peak": 76099584, + "anon": 67178496, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 596 + }, + { + "t": 45.4, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 73740288, + "peak": 76099584, + "anon": 67211264, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 896 + }, + { + "t": 60.5, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 73961472, + "peak": 76099584, + "anon": 67301376, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1192 + }, + { + "t": 75.6, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74129408, + "peak": 76099584, + "anon": 67309568, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1492 + }, + { + "t": 90.7, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74018816, + "peak": 76099584, + "anon": 67297280, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1788 + }, + { + "t": 105.9, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74145792, + "peak": 76099584, + "anon": 67330048, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2088 + }, + { + "t": 121.0, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 73809920, + "peak": 76099584, + "anon": 67350528, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2384 + }, + { + "t": 136.1, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74170368, + "peak": 76099584, + "anon": 67362816, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2684 + }, + { + "t": 151.2, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74055680, + "peak": 76099584, + "anon": 67448832, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2980 + }, + { + "t": 166.3, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74190848, + "peak": 76099584, + "anon": 67469312, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3276 + }, + { + "t": 181.5, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74108928, + "peak": 76242944, + "anon": 67579904, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3576 + }, + { + "t": 196.6, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74395648, + "peak": 76242944, + "anon": 67620864, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3872 + }, + { + "t": 211.7, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74678272, + "peak": 76242944, + "anon": 67706880, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4172 + }, + { + "t": 226.8, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 73949184, + "peak": 76242944, + "anon": 67698688, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4468 + }, + { + "t": 242.0, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74207232, + "peak": 76402688, + "anon": 67756032, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4768 + }, + { + "t": 257.1, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 73666560, + "peak": 76402688, + "anon": 67796992, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5064 + }, + { + "t": 272.2, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74612736, + "peak": 76402688, + "anon": 67846144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5364 + }, + { + "t": 287.3, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74948608, + "peak": 76402688, + "anon": 67846144, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5660 + }, + { + "t": 302.4, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "current": 74383360, + "peak": 76402688, + "anon": 67883008, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5960 + } +] \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/migration-lines.txt b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/migration-lines.txt new file mode 100644 index 00000000..5dfd5965 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/migration-lines.txt @@ -0,0 +1,2 @@ +sabnzbd | [migrations] started +sabnzbd | [migrations] no migrations found \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/run.log b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/run.log new file mode 100644 index 00000000..a2bbc828 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/run.log @@ -0,0 +1,38 @@ +[20:37:02] scratch drive folders cleared before FROM (R-656): ['/srv/felhom-gate/hdd/userdata/media/usenet'] +[20:37:02] MV-sabnzbd: deploying sabnzbd at FROM {'sabnzbd': 'lscr.io/linuxserver/sabnzbd:version-5.1.2'} +[20:37:34] FROM settled=True in 30.8s :: {"sabnzbd": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[20:37:34] sabnzbd: addfile felhom01ddfbf7 http=200 ok=True +[20:37:34] sabnzbd: category felhom01ddfbf7 created +[20:37:34] sabnzbd: job kept, category present=True +[20:37:34] C1 (seed reads back BEFORE): True +[20:37:34] MV-sabnzbd: swapping to TO {'sabnzbd': 'lscr.io/linuxserver/sabnzbd:version-5.1.3'} +[20:37:38] TO up -d rc=0 +[20:38:09] TO settled=True in 31.0s :: {"sabnzbd": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[20:38:09] migration lines observed: 2 +[20:38:10] sabnzbd: job kept, category present=True +[20:38:10] RESULT (seed reads back AFTER): True +[20:38:10] memory watch: 300s, 4 callers on 1 path(s) at 172.18.0.2:8080 +[20:38:25] + 15s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=300 +[20:38:40] + 30s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=596 +[20:38:55] + 45s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=896 +[20:39:10] + 60s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=1192 +[20:39:26] + 76s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=1492 +[20:39:41] + 91s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=1788 +[20:39:56] + 106s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=2088 +[20:40:11] + 121s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=2384 +[20:40:26] + 136s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=2684 +[20:40:41] + 151s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=2980 +[20:40:56] + 166s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=3276 +[20:41:11] + 182s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=3576 +[20:41:27] + 197s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=3872 +[20:41:42] + 212s sabnzbd=71M/1024M peak=72M kills=0 rs=0 reqs=4172 +[20:41:57] + 227s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=4468 +[20:42:12] + 242s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=4768 +[20:42:27] + 257s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=5064 +[20:42:42] + 272s sabnzbd=71M/1024M peak=72M kills=0 rs=0 reqs=5364 +[20:42:57] + 287s sabnzbd=71M/1024M peak=72M kills=0 rs=0 reqs=5660 +[20:43:12] + 302s sabnzbd=70M/1024M peak=72M kills=0 rs=0 reqs=5960 +[20:43:13] memory watch: killed=False tight=[] requests=5960 codes={'303': 5960} +[20:43:13] MV-sabnzbd: ABORT — putting the FROM images back +[20:43:48] sabnzbd: job kept, category present=True +[20:43:48] ABORT: app came back in 30.9s; data present=True \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/to-full.log b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/to-full.log new file mode 100644 index 00000000..cede7325 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/to-full.log @@ -0,0 +1,77 @@ +sabnzbd | [migrations] started +sabnzbd | [migrations] no migrations found +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | ██╗ ███████╗██╗ ██████╗ +sabnzbd | ██║ ██╔════╝██║██╔═══██╗ +sabnzbd | ██║ ███████╗██║██║ ██║ +sabnzbd | ██║ ╚════██║██║██║ ██║ +sabnzbd | ███████╗███████║██║╚██████╔╝ +sabnzbd | ╚══════╝╚══════╝╚═╝ ╚═════╝ +sabnzbd | +sabnzbd | Brought to you by linuxserver.io +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | To support the app dev(s) visit: +sabnzbd | SABnzbd: https://sabnzbd.org/donate +sabnzbd | +sabnzbd | To support LSIO projects visit: +sabnzbd | https://www.linuxserver.io/donate/ +sabnzbd | +sabnzbd | ─────────────────────────────────────── +sabnzbd | GID/UID +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | User UID: 1000 +sabnzbd | User GID: 1000 +sabnzbd | ─────────────────────────────────────── +sabnzbd | Linuxserver.io version: 5.1.3-ls276 +sabnzbd | Build-date: 2026-10-08T06:10:08+00:00 +sabnzbd | ─────────────────────────────────────── +sabnzbd | +sabnzbd | [custom-init] No custom files found, skipping... +sabnzbd | 2026-10-12 22:37:40,111::INFO::[SABnzbd:1135] -------------------------------- +sabnzbd | 2026-10-12 22:37:40,111::INFO::[SABnzbd:1136] SABnzbd.py-5.1.3 +sabnzbd | 2026-10-12 22:37:40,112::INFO::[SABnzbd:1137] Full executable path = /app/sabnzbd/SABnzbd.py +sabnzbd | 2026-10-12 22:37:40,112::INFO::[SABnzbd:1138] Arguments = "/app/sabnzbd/SABnzbd.py" "--config-file" "/config" "--server" "::" +sabnzbd | 2026-10-12 22:37:40,113::INFO::[SABnzbd:1147] Certifi version = 2026.06.17 +sabnzbd | 2026-10-12 22:37:40,113::INFO::[SABnzbd:1148] Loaded additional certificates from /lsiopy/lib/python3.14/site-packages/certifi/cacert.pem +sabnzbd | 2026-10-12 22:37:40,113::INFO::[SABnzbd:1154] Using INI file /config/sabnzbd.ini +sabnzbd | 2026-10-12 22:37:40,115::INFO::[postproc:153] Loading postproc queue +sabnzbd | 2026-10-12 22:37:40,115::INFO::[scheduler:191] Scheduling RSS interval task every 60 min (delay=18) +sabnzbd | 2026-10-12 22:37:40,116::INFO::[scheduler:215] Setting schedule for midnight auto history-purge +sabnzbd | 2026-10-12 22:37:40,116::INFO::[scheduler:224] Setting schedule for midnight BPS reset +sabnzbd | 2026-10-12 22:37:40,116::INFO::[scheduler:233] Setting schedule for midnight server expiration check +sabnzbd | 2026-10-12 22:37:40,116::INFO::[scheduler:242] Setting schedule for server quota check +sabnzbd | 2026-10-12 22:37:40,117::INFO::[__init__:315] All processes started +sabnzbd | 2026-10-12 22:37:40,117::INFO::[SABnzbd:333] Template location for Glitter is /app/sabnzbd/interfaces/Glitter +sabnzbd | 2026-10-12 22:37:40,117::INFO::[SABnzbd:333] Template location for Config is /app/sabnzbd/interfaces/Config +sabnzbd | 2026-10-12 22:37:40,118::INFO::[misc:1469] [N/A] Running external command: ['/usr/bin/unrar'] +sabnzbd | 2026-10-12 22:37:40,120::INFO::[misc:1469] [N/A] Running external command: ['/usr/bin/7zz'] +sabnzbd | 2026-10-12 22:37:40,123::INFO::[misc:1469] [N/A] Running external command: ['/usr/local/bin/par2', '-V'] +sabnzbd | 2026-10-12 22:37:40,126::INFO::[SABnzbd:418] SABCTools module (v9.6.3)... found! +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:419] SABCTools module is using SIMD set: AVX2 +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:420] SABCTools module is linked to OpenSSL: True +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:437] Cryptography module (v50.0.2)... found! +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:440] par2 binary... found (/usr/local/bin/par2) +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:447] UNRAR binary... found (/usr/bin/unrar) +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:455] UNRAR binary version 7.23 +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:462] 7za binary... found (/usr/bin/7zz) +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:464] 7za binary version 26.01 +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:470] nice binary... found (/bin/nice) +sabnzbd | 2026-10-12 22:37:40,127::INFO::[SABnzbd:474] ionice binary... found (/bin/ionice) +sabnzbd | 2026-10-12 22:37:40,128::INFO::[SABnzbd:1351] Starting web-interface on :::8080 +sabnzbd | 2026-10-12 22:37:40,128::INFO::[_cplogging:213] [12/Oct/2026:22:37:40] ENGINE Bus STARTING +sabnzbd | 2026-10-12 22:37:40,231::INFO::[_cplogging:213] [12/Oct/2026:22:37:40] ENGINE Serving on http://:::8080 +sabnzbd | 2026-10-12 22:37:40,232::INFO::[_cplogging:213] [12/Oct/2026:22:37:40] ENGINE Bus STARTED +sabnzbd | 2026-10-12 22:37:40,232::INFO::[SABnzbd:1390] Starting SABnzbd.py-5.1.3 +sabnzbd | 2026-10-12 22:37:40,234::INFO::[dirscanner:121] Dirscanner starting up +sabnzbd | 2026-10-12 22:37:40,234::INFO::[notifier:169] Sending notification: SABnzbd - SABnzbd 5.1.3 started (type=startup, job_cat=None) +sabnzbd | 2026-10-12 22:37:40,235::INFO::[__init__:486] Commit = a647f73426cb70ad60ae7964b96c2d5e939b4727 +sabnzbd | 2026-10-12 22:37:40,235::INFO::[__init__:487] Python-version = 3.14.8 (main, Oct 1 2026, 11:52:26) [GCC 15.2.0] +sabnzbd | 2026-10-12 22:37:40,235::INFO::[__init__:488] CPU architecture = x86_64 +sabnzbd | 2026-10-12 22:37:40,235::INFO::[__init__:489] Platform = Docker +sabnzbd | 2026-10-12 22:37:40,235::INFO::[__init__:490] JSON-module = orjson 3.11.9 +sabnzbd | 2026-10-12 22:37:40,235::INFO::[__init__:491] Preferred encoding = UTF-8 +sabnzbd | 2026-10-12 22:37:40,235::INFO::[__init__:492] SSL version = OpenSSL 3.5.9 29 Sep 2026 +sabnzbd | [ls.io-init] done. diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/to-states.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/to-states.json new file mode 100644 index 00000000..1ba69f9f --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/to-states.json @@ -0,0 +1,8 @@ +{ + "sabnzbd": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/verdict.json b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/verdict.json new file mode 100644 index 00000000..370ad5db --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/sabnzbd-bench/MV-sabnzbd/verdict.json @@ -0,0 +1,58 @@ +{ + "harness_version": 5, + "edge": "MV-sabnzbd", + "app": "sabnzbd", + "note": "night 2026-09-23 within-a-major move: sabnzbd=lscr.io/linuxserver/sabnzbd:version-5.1.3", + "from": { + "sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.2" + }, + "to": { + "sabnzbd": "lscr.io/linuxserver/sabnzbd:version-5.1.3" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "migration_observed": "\u001b[2Ksabnzbd | [migrations] started", + "abort": "starts-and-serves", + "abort_detail": null, + "engine_state_after": null, + "memory": { + "soak_s": 302.7, + "requested_s": 300, + "requests": 5960, + "codes": { + "303": 5960 + }, + "first_kill": null, + "containers": { + "sabnzbd": { + "limit": 1073741824, + "peak": 76402688, + "peak_pct": 0.071, + "anon_peak_sampled": 67883008, + "anon_peak_pct": 0.063, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + } + }, + "unmeasured": [], + "venue_swap_bytes": 0, + "load": "reached" + }, + "marks": [], + "bench_overrides": null, + "duration_s": 31.0, + "measured_at": "2026-10-12T20:43:48Z", + "evidence": "evidence/MV-sabnzbd", + "scratch_cleared": [ + "/srv/felhom-gate/hdd/userdata/media/usenet" + ], + "files_changed": [], + "files_changed_detail": [], + "files_ignored": [], + "total_s": 405.5 +} \ No newline at end of file diff --git a/documentation/audits/usenet-2026-10-12/tools/boxstep_sabnzbd_again.py b/documentation/audits/usenet-2026-10-12/tools/boxstep_sabnzbd_again.py new file mode 100644 index 00000000..462c7047 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/tools/boxstep_sabnzbd_again.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""SABnzbd's box step, run again after the fixture was fixed. + +The first attempt could not seed: `walk.GENERATED` holds no optional `type: secret` field, so the +fixture had no API key (the controller generates that one itself). The fixture reads it from the +app's own app.yaml now - the same place the household reads it. + +The app is already installed at the TO pin from that attempt, so the step starts by removing it and +staging the FROM pin again: a ladder step has to begin where a household begins. +""" +import json +import os +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools") +sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") +os.environ.setdefault("SC", "/tmp/usenet-walk") + +import walk as w # noqa: E402 +import boxwalk_usenet as bw # noqa: E402 +import boxstep_usenet as bs # noqa: E402 + +APP, SUB = "sabnzbd", "usenet" + +w.login() +st = w.stack(APP) +if st.get("deployed"): + w.say("removing the install from the first attempt (a ladder step starts where a household starts)") + w.remove(APP) + time.sleep(5) + +w.say("staging the FROM pin again") +import stage_in_cache as stage # noqa: E402 +stage.stage("frm") +time.sleep(5) + +ok = w.deploy(APP, SUB, extra_values={"HDD_PATH": bw.DRIVE_ROOT}) +w.say(f"deploy -> {ok}") +if not ok: + raise SystemExit("the install did not succeed") +w.say(f"the app answers: {w.wait_app(SUB, '/', want=('200', '303', '401', '403'), tries=40, delay=5)}") + +# the key the household was given, read the way the fixture now reads it +import upgrade_fixtures_box as fxb # noqa: E402 +fx = fxb.FIXTURES[APP] +w.say(f"the API key is readable: {bool(fx._key(w))} (length {len(fx._key(w))})") + +v = bs.step(APP) +json.dump(v, open(os.path.join(bs.EV, "box-steps-sabnzbd.json"), "w"), indent=2, ensure_ascii=False) +print(json.dumps(v, indent=2, ensure_ascii=False)) diff --git a/documentation/audits/usenet-2026-10-12/tools/boxstep_usenet.py b/documentation/audits/usenet-2026-10-12/tools/boxstep_usenet.py new file mode 100644 index 00000000..f5044924 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/tools/boxstep_usenet.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""The BOX venue for the two Usenet clients: the walk, then the ladder's first step, in ONE process. + +One process because the generated deploy values (the household's own password and API key) live in +`walk.GENERATED`, in memory - the box fixtures need them to seed and to read back, so a separate run +would have nothing to sign in with. + +Per app: + 1. the walk (boxwalk_usenet): install from the staged FROM pin, then the stranger/household probes + 2. prep.txt - the pinned images the install is actually running + 3. the box fixture SEEDS through the app's own interface, as the household + 4. the staged template moves to the TO pin and the box rescans (this is what `drill_bump` does to + the drill repo; that repo is private and this box has no catalog credentials, see + stage_in_cache.py, so the same move is made in the box's own catalog cache) + 5. the product's own guarded Update is PRESSED and every phase recorded + 6. the box fixture READS BACK, with its negative control + 7. box-verdict-<app>.json - `proven` only if the update ended `done`, the app is healthy, and the + readback says the data survived. That file is what `upgrade-test.py --write-ladder` consumes, + and it refuses to write a ladder entry without it. + +Runs ON DOOPLEX. +""" +import json +import os +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +TOOLS = "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools" +sys.path.insert(0, TOOLS) +sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") + +os.environ.setdefault("SC", "/tmp/usenet-walk") +import walk as w # noqa: E402 +import boxwalk_usenet as bw # noqa: E402 +import upgrade_fixtures_box as fxb # noqa: E402 +import stage_in_cache as stage # noqa: E402 + +EV = bw.EV +PINS = stage.PINS +SUBS = {"sabnzbd": "usenet", "nzbget": "nzbget"} + + +def pinned(app): + st = w.stack(app) + return st.get("pinned_images") or st.get("images") or {} + + +def step(app): + sub = SUBS[app] + frm, to = PINS[app] + d = os.path.join(EV, app) + os.makedirs(d, exist_ok=True) + log = [] + + def say(line): + log.append(line) + w.say(line) + + say(f"before: pinned={pinned(app)}") + fx = fxb.FIXTURES[app] + + # 3 — seed as the household, through the app's own interface + seeded = fx.seed(w, sub, say) + if not seeded: + open(os.path.join(d, "step.txt"), "w").write("\n".join(log) + "\nSEED FAILED\n") + return {"app": app, "verdict": "inconclusive", "why": "the box fixture could not seed"} + + # 4 — the catalog moves to the TO pin (staged, because the drill repo is unreachable) + stage.stage("to") + say(f"staged the catalog at the TO pin: {to}") + w.ctl("POST", "/api/stacks/rescan") + time.sleep(5) + say(f"catalog_images now: {w.stack(app).get('catalog_images')}") + + # 5 — the product's own Update + res = w.press_update(app) + for ph in res.get("phases", []): + say(f" phase +{ph['t']}s {ph['phase']} | err={ph['error']}") + say(f"update accepted={res.get('accepted')} final={res.get('final_phase')} " + f"err={res.get('update_error')} {res.get('duration_s')}s") + + # 6 — the readback, with the fixture's own negative control + ok = fx.verify(w, sub, seeded, say) + obs = w.observables(app) + say(f"observables: {json.dumps(obs)[:300]}") + say(f"after: pinned={pinned(app)}") + + verdict = { + "app": app, + "venue": "box 9202 (templates staged in the box's catalog cache - the drill repo is private " + "and the box has no catalog credentials), the product's guarded Update", + "from": {app: frm}, + "to": {app: to}, + "verdict": "proven" if (ok and res.get("final_phase") == "done") else "inconclusive", + "seed_read_before": True, + "seed_read_after": bool(ok), + "healthy_after": bool(obs.get("health") in ("healthy", None) and obs.get("state") == "running") + if isinstance(obs, dict) else None, + "duration_s": res.get("duration_s"), + "final_phase": res.get("final_phase"), + "update_error": res.get("update_error"), + "measured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "evidence": f"felhom.eu/documentation/audits/usenet-2026-10-12/box/{app}/step.txt", + } + open(os.path.join(d, "step.txt"), "w").write("\n".join(log) + "\n") + json.dump(verdict, open(os.path.join(d, f"box-verdict-{app}.json"), "w"), indent=2, ensure_ascii=False) + say(f"=== {app}: {verdict['verdict']} ===") + return verdict + + +def main(): + w.login() + bw.main() # the walk: install + the stranger/household probes + out = {} + for app in ("sabnzbd", "nzbget"): + if (bw.OUT.get(app) or {}).get("deploy"): + out[app] = step(app) + else: + out[app] = {"app": app, "verdict": "not-attempted", "why": "the install did not succeed"} + json.dump(out, open(os.path.join(EV, "box-steps.json"), "w"), indent=2, ensure_ascii=False) + print(json.dumps(out, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/documentation/audits/usenet-2026-10-12/tools/boxwalk_usenet.py b/documentation/audits/usenet-2026-10-12/tools/boxwalk_usenet.py new file mode 100644 index 00000000..5a44f454 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/tools/boxwalk_usenet.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""boxwalk_usenet.py — sabnzbd and nzbget installed on guest 9202 THROUGH THE PRODUCT. + +Closes the checklist items only a real install can close: 3.5 (what a stranger reaches in the +install window), 3.6 (can a stranger lock the household out), 3.9 (sign in the way each app's own +client does, through traefik over https), 3.10 (the visitor's address), 2.6 (remove with data / keep +data), 9.2 (a fresh install as household and as stranger). + +The two apps authenticate differently and the walk follows each: + * SABnzbd's web UI is SESSION-based (sabnzbd/security.py validate_session), so a sign-in POSTs to + /login/ and keeps the cookie - `curl -u` proves nothing there. Its API authenticates with the + generated key instead, which is the route Radarr and Sonarr take. + * NZBGet uses Basic auth on every request. + +EVIDENCE, NOT PRODUCT. It presses the buttons a person presses. Runs ON DOOPLEX; 9202 must already be +pointed at the drill catalog (09 §6.5). +""" +import json +import os +import sys + +TOOLS = "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools" +sys.path.insert(0, TOOLS) +os.environ.setdefault("SC", os.environ.get("SC", "/tmp/usenet-walk")) +os.makedirs(os.environ["SC"], exist_ok=True) +import walk as w # noqa: E402 + +EV = os.environ.get("EV", "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/usenet-2026-10-12/box") +os.makedirs(EV, exist_ok=True) + +# the scratch guest's drive, as the controller names it: a `type: path` field must be a DRIVE, not a +# folder on it (the controller answers 409 to a folder — measured 2026-10-12) +DRIVE_ROOT = os.environ.get("DRIVE_ROOT", "/mnt/felhom-drives/scratch_hdd") + +OUT = {} + + +def rec(app, key, value): + OUT.setdefault(app, {})[key] = value + w.say(f" [{app}] {key}: {str(value)[:170]}") + + +def code_of(args): + r = w.sh(args) + return (r.stdout or "").strip() + + +def stranger(sub, path): + """A stranger: the app's public name, no cookie, no credentials.""" + return code_of(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}{path}"]) + + +def walk_sabnzbd(): + name, sub = "sabnzbd", "usenet" + w.say(f"=== {name} ===") + if not w.stack(name): + rec(name, "template_visible", False) + return + rec(name, "template_visible", True) + ok = w.deploy(name, sub, extra_values={"HDD_PATH": DRIVE_ROOT}) + rec(name, "deploy", ok) + if not ok: + return + gen = w.GENERATED.get(name) or {} + pw, key = gen.get("SAB_ADMIN_PASSWORD", ""), gen.get("SAB_API_KEY", "") + rec(name, "generated_password_length", len(pw)) + rec(name, "generated_api_key_length", len(key)) + rec(name, "app_answers_on_its_public_name", + w.wait_app(sub, "/", want=("200", "303", "401", "403"), tries=40, delay=5)) + + # 3.5 — the stranger. THE question for this app: it has no login of its own on a fresh install, + # and the template's `local_ranges` is what makes its own login apply to a proxied visitor. + rec(name, "stranger_root", stranger(sub, "/")) + rec(name, "stranger_config_page", stranger(sub, "/config/")) + rec(name, "stranger_wizard", stranger(sub, "/wizard/")) + rec(name, "stranger_api_no_key", stranger(sub, "/api?mode=queue&output=json")) + body = w.sh(["curl", "-sSkL", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", + f"{w.BASE}/"]).stdout or "" + title = "" + if "<title>" in body: + title = body.split("<title>", 1)[1].split("<", 1)[0].strip() + rec(name, "stranger_lands_on", title[:60]) + + # 3.9 — the household signs in the way the app's own interface does: POST /login/, keep the cookie + jar = os.path.join(os.environ["SC"], "sab.jar") + w.sh(["rm", "-f", jar]) + rec(name, "household_login_post", code_of( + ["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", "-c", jar, + "--data-urlencode", "username=admin", "--data-urlencode", f"password={pw}", + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}/login/"])) + rec(name, "household_config_page_with_the_cookie", code_of( + ["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", "-b", jar, + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}/config/"])) + badjar = os.path.join(os.environ["SC"], "sab-bad.jar") + w.sh(["rm", "-f", badjar]) + rec(name, "wrong_password_login_post", code_of( + ["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", "-c", badjar, + "--data-urlencode", "username=admin", "--data-urlencode", "password=definitely-not-it", + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}/login/"])) + rec(name, "wrong_password_then_config_page", code_of( + ["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", "-b", badjar, + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}/config/"])) + + # the *arr path: the API with the generated key, and a wrong one + r = w.sh(["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", + f"{w.BASE}/api?mode=version&output=json&apikey={key}"]) + rec(name, "api_with_the_key", (r.stdout or "").strip()[:60]) + r = w.sh(["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", + f"{w.BASE}/api?mode=queue&output=json&apikey=deadbeefdeadbeef"]) + rec(name, "api_with_a_wrong_key", (r.stdout or "").strip()[:60]) + + # 3.6 — six wrong sign-ins, then the household from the same address + codes = [] + for i in range(6): + codes.append(code_of(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "--data-urlencode", "username=admin", + "--data-urlencode", f"password=wrong{i}", + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}/login/"])) + rec(name, "six_wrong_tries", codes) + jar2 = os.path.join(os.environ["SC"], "sab2.jar") + w.sh(["rm", "-f", jar2]) + rec(name, "household_right_after_the_wrong_tries", code_of( + ["curl", "-sSk", "--max-time", "25", "-H", f"Host: {sub}.{w.DOMAIN}", "-c", jar2, + "--data-urlencode", "username=admin", "--data-urlencode", f"password={pw}", + "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}/login/"])) + + # what the app has for the three settings after_install reads back, and what it logged + cfg = w.guest("docker exec sabnzbd sh -c 'grep -E \"^(inet_exposure|local_ranges|username|host_whitelist)\" " + "/config/sabnzbd.ini'") + open(f"{EV}/sabnzbd-settings.txt", "w").write(cfg) + rec(name, "settings_captured", cfg.replace("\n", " | ")[:200]) + logs = w.guest("docker logs --tail 60 sabnzbd 2>&1 | grep -viE 'password|api.?key' | tail -25") + open(f"{EV}/sabnzbd-app-log.txt", "w").write(logs) + paths = w.guest(f"ls -la {w.DRIVE}/media/usenet 2>&1 | head -8; " + "docker volume ls --format '{{.Name}}' | grep -i sab | head -4") + open(f"{EV}/sabnzbd-paths.txt", "w").write(paths) + + +def walk_nzbget(): + name, sub = "nzbget", "nzbget" + w.say(f"=== {name} ===") + if not w.stack(name): + rec(name, "template_visible", False) + return + rec(name, "template_visible", True) + ok = w.deploy(name, sub, extra_values={"HDD_PATH": DRIVE_ROOT}) + rec(name, "deploy", ok) + if not ok: + return + pw = (w.GENERATED.get(name) or {}).get("NZB_ADMIN_PASSWORD", "") + rec(name, "generated_password_length", len(pw)) + rec(name, "app_answers_on_its_public_name", + w.wait_app(sub, "/", want=("200", "401"), tries=40, delay=5)) + + rec(name, "stranger_root", stranger(sub, "/")) + rec(name, "stranger_rpc", stranger(sub, "/jsonrpc/version")) + + def basic(password, path="/"): + return code_of(["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-u", f"admin:{password}", "-o", "/dev/null", "-w", "%{http_code}", + f"{w.BASE}{path}"]) + + rec(name, "household_signin_through_traefik", basic(pw)) + rec(name, "wrong_password_through_traefik", basic("definitely-not-it")) + # THE DOCUMENTED DEFAULT the app ships with, which every Usenet guide carries + rec(name, "the_documented_default_login", basic("tegbzn6789")) + rec(name, "the_default_name_and_our_password", code_of( + ["curl", "-sSk", "--max-time", "20", "-H", f"Host: {sub}.{w.DOMAIN}", + "-u", f"nzbget:{pw}", "-o", "/dev/null", "-w", "%{http_code}", f"{w.BASE}/"])) + rec(name, "household_rpc_version", basic(pw, "/jsonrpc/version")) + + codes = [basic(f"wrong{i}") for i in range(6)] + rec(name, "six_wrong_tries", codes) + rec(name, "household_right_after_the_wrong_tries", basic(pw)) + + cfg = w.guest("docker exec nzbget sh -c 'grep -E \"^(MainDir|DestDir|InterDir|QueueDir|TempDir|UpdateCheck" + "|ArticleCache|WriteBuffer|ControlUsername)=\" /config/nzbget.conf'") + open(f"{EV}/nzbget-settings.txt", "w").write(cfg) + rec(name, "settings_captured", cfg.replace("\n", " | ")[:220]) + rec(name, "the_well_known_default_left_in_the_file", + w.guest("docker exec nzbget sh -c 'grep -c tegbzn6789 /config/nzbget.conf'").strip()) + logs = w.guest("docker logs --tail 60 nzbget 2>&1 | tail -20") + open(f"{EV}/nzbget-app-log.txt", "w").write(logs) + paths = w.guest(f"ls -la {w.DRIVE}/media/nzbget 2>&1 | head -8; " + "docker exec nzbget sh -c 'ls /config/data' 2>&1 | head -8; " + "docker volume ls --format '{{.Name}}' | grep -i nzb | head -4") + open(f"{EV}/nzbget-paths.txt", "w").write(paths) + + +def main(): + w.login() + w.say("logged in to the 9202 dashboard") + # RESCAN ONLY, never sync: the two templates are STAGED in the box's catalog cache (the drill + # repo is private and this box has no catalog credentials - see stage_in_cache.py), and a sync + # re-clones that cache from the published catalog, which would delete them mid-walk. + code, d = w.ctl("POST", "/api/stacks/rescan", {}) + w.say(f"rescan -> {code} {str(d)[:120]}") + + walk_sabnzbd() + walk_nzbget() + + json.dump(OUT, open(f"{EV}/boxwalk-results.json", "w"), indent=2, ensure_ascii=False) + open(f"{EV}/boxwalk-log.txt", "w").write("\n".join(w.LOG)) + print(json.dumps(OUT, indent=2, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/documentation/audits/usenet-2026-10-12/tools/stage_in_cache.py b/documentation/audits/usenet-2026-10-12/tools/stage_in_cache.py new file mode 100644 index 00000000..3fb64a15 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/tools/stage_in_cache.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Stage two WIP templates in guest 9202's own catalog cache, because the drill route is broken. + +WHY NOT THE DRILL REPO (`09` §6.5). `admin/app-catalog-drill` is PRIVATE (Gitea API, 2026-10-12: +private=true, while the live catalog is private=false), and no controller.yaml on 9202 - current or +any of the 30 saved copies - has ever carried `git.username`/`git.token`. So the box cannot clone it: +`fatal: could not read Username for 'https://gitea.dooplex.hu'`, exit 128, measured. The route worked +for the campaigns of 2026-10-10 and this afternoon, so the repo's visibility changed since; either +way the documented procedure cannot be followed today without putting a credential on the box, which +is an operator decision and not one for a session to take. + +WHAT THIS DOES INSTEAD, and why it is the same test. The controller reads its templates from the +cache it clones into; `drill_bump` works by editing the repo and letting the sync copy the change +into that cache. This writes the same files into the same place directly, so the box deploys, scans, +badges and updates exactly as it would from a catalog - and the only thing not exercised is the git +fetch itself, which is not what a new app's box walk is measuring. + +Two guards, because a staged file that a sync overwrites mid-walk would be a silent mess: + * the sync interval goes to 12h for the duration (restored at teardown), and + * the walk calls rescan, never sync, while the staging is in place. + + python3 stage_in_cache.py # stage both at their FROM pins + python3 stage_in_cache.py --to # move the staged pair to the TO pins (the ladder's box step) + python3 stage_in_cache.py --status # what the cache holds now +""" +import os +import sys +import time + +os.environ.setdefault("SC", "/tmp/usenet-walk") +sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools") +import walk as w # noqa: E402 + +D = "/opt/docker/felhom-controller" +CACHE = D + "/data/catalog-cache/templates" +SRC = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill/templates" # the WIP pair lives here +APPS = ("sabnzbd", "nzbget") +PINS = { + "sabnzbd": ("lscr.io/linuxserver/sabnzbd:version-5.1.2", "lscr.io/linuxserver/sabnzbd:version-5.1.3"), + "nzbget": ("nzbgetcom/nzbget:v26.2", "nzbgetcom/nzbget:v26.3"), +} + + +def status(): + print("repo_url:", w.guest("docker exec felhom-controller sh -c 'grep repo_url %s/controller.yaml'" % D).strip()) + print("sync interval:", w.guest("docker exec felhom-controller sh -c 'grep sync_interval %s/controller.yaml'" % D).strip()) + print("templates in the cache:", + w.guest("docker exec felhom-controller sh -c 'ls %s 2>/dev/null | wc -l'" % CACHE).strip()) + for a in APPS: + print(" %-9s %s" % (a, w.guest("docker exec felhom-controller sh -c " + "'grep -h \"^ image:\" %s/%s/docker-compose.yml 2>/dev/null'" + % (CACHE, a)).strip() or "(not staged)")) + + +def stage(which): + for a in APPS: + frm, to = PINS[a] + ref = to if which == "to" else frm + # the files go in through the guest, then into the container's volume + for f in ("docker-compose.yml", ".felhom.yml"): + local = "%s/%s/%s" % (SRC, a, f) + body = open(local).read() + if f == "docker-compose.yml": + body = body.replace("image: " + to, "image: " + ref) + # written through the guest's own shell, base64 so no quoting level can mangle it + import base64 + b64 = base64.b64encode(body.encode()).decode() + # TWO places, because the sync writes both: the cache is what the update badge and + # `catalog_images` are read from, and /opt/docker/stacks/<app>/ is what makes the stack + # exist for the dashboard and the deploy endpoint at all (measured: staging only the + # cache left the app invisible). + w.guest("docker exec felhom-controller sh -c 'mkdir -p %s/%s'" % (CACHE, a)) + w.guest("echo %s | base64 -d > /tmp/stage.tmp && docker cp /tmp/stage.tmp " + "felhom-controller:%s/%s/%s && rm -f /tmp/stage.tmp" % (b64, CACHE, a, f)) + w.guest("mkdir -p /opt/docker/stacks/%s && echo %s | base64 -d > /opt/docker/stacks/%s/%s" + % (a, b64, a, f)) + print(" staged %-9s at %s" % (a, ref)) + code, d = w.ctl("POST", "/api/stacks/rescan") + print(" rescan -> %s %s" % (code, str(d)[:80])) + time.sleep(6) + for a in APPS: + st = w.stack(a) + print(" %-9s visible=%-5s catalog_images=%s" % (a, bool(st), st.get("catalog_images"))) + + +def slow_the_sync(): + cur = w.guest("docker exec felhom-controller sh -c 'grep sync_interval %s/controller.yaml'" % D).strip() + if "12h" in cur: + print(" the sync interval is already 12h") + return + w.guest("docker exec felhom-controller sh -c \"sed -i 's|sync_interval: .*|sync_interval: 12h|' " + "%s/controller.yaml\"" % D) + print(" sync interval: %s -> 12h (the teardown restores it)" % cur.split(":", 1)[-1].strip()) + w.guest("docker restart felhom-controller") + time.sleep(25) + w.login() + + +def back_to_the_live_catalog(): + """The box must be on a catalog it CAN clone, so the cache exists at all - the live one is + public. The staged pair is then written into that cache beside the 62 published templates.""" + w.guest("docker exec felhom-controller sh -c 'cp %s/controller.yaml.pre-usenet1012 %s/controller.yaml'" % (D, D)) + print(" repo_url:", w.guest("docker exec felhom-controller sh -c 'grep repo_url %s/controller.yaml'" % D).strip()) + w.guest("docker exec felhom-controller sh -c 'rm -rf %s/data/catalog-cache'" % D) + w.guest("docker restart felhom-controller") + time.sleep(25) + w.login() + code, d = w.ctl("POST", "/api/sync") + print(" sync -> %s %s" % (code, str(d)[:100])) + time.sleep(15) + n = w.guest("docker exec felhom-controller sh -c 'ls %s 2>/dev/null | wc -l'" % CACHE).strip() + print(" the cache now holds %s templates (the published catalog)" % n) + return n not in ("", "0") + + +if __name__ == "__main__": + w.login() + if "--status" in sys.argv: + status() + elif "--restage" in sys.argv: # the catalog pointer and the interval are already set + stage("frm") + status() + elif "--to" in sys.argv: + stage("to") + else: + if not back_to_the_live_catalog(): + raise SystemExit("the live catalog did not populate the cache - nothing was staged") + slow_the_sync() + stage("frm") + status() diff --git a/documentation/audits/usenet-2026-10-12/tools/teardown_usenet.py b/documentation/audits/usenet-2026-10-12/tools/teardown_usenet.py new file mode 100644 index 00000000..fe2d5357 --- /dev/null +++ b/documentation/audits/usenet-2026-10-12/tools/teardown_usenet.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Teardown after the Usenet campaign. Three layers, each verified, and the control for the catalog +layer comes from a different channel than the controller's own verdict (R-286). + +What this campaign changed on 9202 and therefore has to put back: + 1. the two apps are installed -> removed THROUGH THE PRODUCT, volumes and drive folders + 2. two templates are STAGED in the cache -> deleted from the cache AND from /opt/docker/stacks + 3. the sync interval was raised to 12h -> back to 15m + 4. the catalog pointer -> already the live one (the staging needed it); verified +""" +import json +import os +import sys +import time + +os.environ.setdefault("SC", "/tmp/usenet-walk") +sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/felhom.eu/documentation/audits/pg-last-six-2026-09-30/tools") +import walk as w # noqa: E402 + +D = "/opt/docker/felhom-controller" +CACHE = D + "/data/catalog-cache/templates" +APPS = ("sabnzbd", "nzbget") +OUT = {} + +w.login() + +# ---------------------------------------------------------------- 1. the apps, through the product +for app in APPS: + st = w.stack(app) + if not st.get("deployed"): + w.say("[X] %s: not deployed" % app) + OUT[app] = {"was_deployed": False} + continue + code = w.remove(app) + OUT[app] = {"was_deployed": True, "remove_code": code, + "deployed_after": w.stack(app).get("deployed")} + +# ---------------------------------------------------------------- 2. the staged templates +before = w.guest("docker exec felhom-controller sh -c 'ls %s | wc -l'" % CACHE).strip() +for app in APPS: + w.guest("docker exec felhom-controller sh -c 'rm -rf %s/%s'" % (CACHE, app)) + w.guest("rm -rf /opt/docker/stacks/%s" % app) +after = w.guest("docker exec felhom-controller sh -c 'ls %s | wc -l'" % CACHE).strip() +left = w.guest("docker exec felhom-controller sh -c 'ls %s | grep -E \"sab|nzb\" | tr \"\\n\" \" \"'" % CACHE).strip() +stacks_left = w.guest("ls -d /opt/docker/stacks/sabnzbd /opt/docker/stacks/nzbget 2>/dev/null").strip() +OUT["staging"] = {"cache_before": before, "cache_after": after, + "staged_names_left": left or "(none)", "stack_dirs_left": stacks_left or "(none)"} +w.say("[S] cache %s -> %s templates; staged names left: %r; stack dirs left: %r" + % (before, after, left, stacks_left)) + +# ---------------------------------------------------------------- 3. the sync interval +cur = w.guest("docker exec felhom-controller sh -c 'grep sync_interval %s/controller.yaml'" % D).strip() +w.guest("docker exec felhom-controller sh -c \"sed -i 's|sync_interval: .*|sync_interval: 15m|' %s/controller.yaml\"" % D) +w.guest("docker restart felhom-controller") +time.sleep(25) +w.login() +now = w.guest("docker exec felhom-controller sh -c 'grep sync_interval %s/controller.yaml'" % D).strip() +OUT["sync_interval"] = {"before": cur, "after": now} +w.say("[I] sync interval %r -> %r" % (cur, now)) + +# ---------------------------------------------------------------- 4. the catalog pointer, from a different channel +repo = w.guest("docker exec felhom-controller sh -c 'grep repo_url %s/controller.yaml'" % D).strip() +w.sync_rescan() +time.sleep(10) +import subprocess +CAT = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu" +subprocess.run(["git", "-C", CAT, "fetch", "-q", "origin"], capture_output=True) +checks = {} +for app in ("sabnzbd", "nzbget", "grocy"): + pushed = subprocess.run(["bash", "-c", + "git -C %s show origin/main:templates/%s/.felhom.yml | sha256sum | cut -c1-16" % (CAT, app)], + capture_output=True, text=True).stdout.strip() + onbox = w.guest("docker exec felhom-controller sh -c 'sha256sum %s/%s/.felhom.yml 2>/dev/null'" % (CACHE, app)) + onbox = (onbox.split() or [""])[0][:16] + checks[app] = {"published": pushed, "on_the_box": onbox, "same": pushed == onbox} + w.say("[C] %-9s published=%s box=%s %s" % (app, pushed, onbox or "(absent)", + "SAME" if pushed == onbox else "DIFFERENT")) +decoy = subprocess.run(["bash", "-c", "git -C %s show origin/main:README.md | sha256sum | cut -c1-16" % CAT], + capture_output=True, text=True).stdout.strip() +checks["decoy_readme_vs_sabnzbd_template"] = {"same": decoy == checks["sabnzbd"]["on_the_box"]} +OUT["catalog"] = {"repo_url": repo, "templates_in_cache": w.guest( + "docker exec felhom-controller sh -c 'ls %s | wc -l'" % CACHE).strip(), "sha_checks": checks} + +# ---------------------------------------------------------------- what is left running +OUT["containers"] = w.guest("docker ps --format '{{.Names}}' | sort | tr '\\n' ' '").strip() +OUT["volumes_matching"] = w.guest("docker volume ls --format '{{.Name}}' | grep -E 'sab|nzb' | tr '\\n' ' '").strip() +OUT["drive_folders"] = w.guest("ls -d /mnt/felhom-drives/scratch_hdd/userdata/media/usenet " + "/mnt/felhom-drives/scratch_hdd/userdata/media/nzbget 2>/dev/null").strip() or "(gone)" +w.say("[R] containers: " + OUT["containers"]) +w.say("[R] volumes matching: " + (OUT["volumes_matching"] or "(none)")) +w.say("[R] drive folders: " + OUT["drive_folders"]) + +p = os.path.join(os.environ["SC"], "teardown-usenet.json") +json.dump(OUT, open(p, "w"), indent=2, ensure_ascii=False) +print("WROTE " + p) diff --git a/documentation/backlog/CLOSED-ITEMS.md b/documentation/backlog/CLOSED-ITEMS.md index e6bd39c1..24770444 100644 --- a/documentation/backlog/CLOSED-ITEMS.md +++ b/documentation/backlog/CLOSED-ITEMS.md @@ -33,6 +33,7 @@ closed. Private evidence (DooPlex only): `/mnt/5_hdd/felhom.eu/evidence/security | Row | What | Closed | Evidence | |---|---|---|---| +| **R-988** | **The Usenet pair the operator asked for: SABnzbd and NZBGet.** (P4) | CLOSED 2026-10-12 — **both published** (`app-catalog-felhom.eu` 8ee687e) with their complete onboarding records, a first ladder step proven on the bench AND on 9202 through the product, README + FIRST-ADMIN rows, app cards and the published count 60 → 62. SABnzbd is the second third-party-published image in the catalog (the project publishes none). Nine defects were found by measuring and fixed before publication. **Reasoning kept:** a household must bring a PAID Usenet provider, so the download chain and memory under that load are NOT measured and neither record claims they are; what is measured is each app's queue taking a job through its own API and keeping it across the product's Update. **Rule kept:** `inet_exposure` and its like protect nobody while the proxy's address counts as local; a `backup`/`data_paths` entry must name a path the compose actually binds, or the controller rejects the block and the data has no class at all. | `audits/usenet-2026-10-12/` (both bench runs, both box steps, nine defects with their measurements) | | **R-950** | **The hub's operator/global key was readable in public git history and still worked — anyone the hub's ingress let in could act as the operator for every box (including reading the vaulted break-glass credentials).** (P1) | CLOSED 2026-10-12 — **rotated** (operator GO in session): the runbook rotation in `runbooks/secrets.md` („Operator/global bearer key", steps 1–4). Before: the old key answered 200. After: new key 200, old key 403/401; all three boxes reported normally after the restart. The only consumer was `Deployment/hub`. Whether anyone used the old key before today is R-954's question. | PRIVATE/SR-01; `runbooks/secrets.md` step 5 note | | **R-951** | **Three hub API read routes answered with no credential: anyone the hub's ingress let in could list every customer and read each box's full report.** (P1) | CLOSED 2026-10-12 — **fixed in hub v0.148.0**: the operator/global key only (no key, a box key or another household's key → 403); the operator response is unchanged. | `TestCustomersRoutes_NoCredentialIsRefused` (red-proved), `TestCustomersRoutes_BoxKeysAreRefused`, `TestCustomersRoutes_GlobalKeyUnchanged`; PRIVATE/SR-02; live check in `audits/security-review-2026-10-12/evidence/` | diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index e22df069..82b75a6a 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -123,7 +123,7 @@ stopping line that lies. | **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC | | **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator | -## Apps & catalog — 7 rows (P2 1, P3 2, P4 4) +## Apps & catalog — 6 rows (P2 1, P3 2, P4 3) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -133,7 +133,6 @@ stopping line that lies. | **R-76** | Apps & catalog | P4 | **FileBrowser-created folders break the setgid chain, and a drop-zone's mode is not stable** **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-26, size S, roadmap state `idea (surfaced by the R-75 spike, 2026-07-26)`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-06 night: not a catalog fix** — the setgid chain is set by the controller's FileBrowser setup; a controller row. | — | Two related findings from `audits/SPIKE-catalog-data-paths-2026-07-26.md` P3/P5, both **pre-existing** and deliberately left alone by that spike. **(a)** FileBrowser Quantum 1.3.3 creates files `0644` and folders `0755` and does **not** propagate the setgid bit — even though the entrypoint wrapper's `umask 002` really is in effect (`/proc/1/status` `Umask: 0002`). Group inheritance itself works (a file uploaded into a 2775 group-100 dir landed group 100, not the process gid 1000), so the convention's *group* half holds and only its *mode* half is lost. The consequence is proven with a control: inside a UI-created `0755` folder a gid-1000 process's file landed group **1000**, while the identical write into the 2775 parent landed group **100**. So **any folder a customer creates through FileBrowser breaks the shared-group chain one level down.** Latent today — every userdata-touching catalog app that declares an identity declares uid/gid **1000**, the same uid FileBrowser runs as, so owner permissions mask it; it bites the day a content app runs as a different non-root uid with gid 1000. The comment at `infra/infra.go:156` is right that the image ignores `-e UMASK` but does not say t | CC | | **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC | | **R-905** | Apps & catalog | P4 | **wger's collected style files (283 MB) go into every wger backup, though the app rebuilds them at every start.** Since R-762's fix (`DJANGO_DEBUG=False` + `collectstatic`, catalog `cf1ed43`) the static files sit in a named volume that the backup legs copy like data (measured 283 MB, `audits/design-build-2026-10-06/`); `collectstatic` regenerates them, so they are rebuildable bytes in every Tier-1 unit, Tier-2 copy and off-site snapshot. wger is `lifecycle: hidden` and no box runs it (hub read 2026-10-08). Options to weigh: a non-backed-up volume class for regenerable data, or an anonymous volume. Found by the R-762 helpers 2026-10-08. | **OPEN — needs a design before wger is shown** | — | Design the „regenerable volume" exclusion (catalog + controller backup legs) | CC | -| **R-988** | Apps & catalog | P4 | **The Usenet pair the operator asked for on 2026-10-12: SABnzbd and NZBGet.** Same shape as the torrent pair published the same day (`app-catalog-felhom.eu` `9081a47`) and the same pipeline: a template each, a bench run on LXC 9401, a box walk on 9202 through the product, a first ladder step written by `upgrade-test.py --write-ladder`, a complete onboarding record, README + FIRST-ADMIN rows, and the app cards. Two things are known already and decide the shape: **SABnzbd publishes no image of its own** (so `lscr.io/linuxserver/sabnzbd`, the same third-party exception Transmission got, which the operator granted on 2026-10-12), while **NZBGet has the project's own `nzbgetcom/nzbget`**; and **both need a PAID Usenet provider (server, port, username, password), which the household must bring** - so the app page has to say so before the install, and a provider's credentials are deploy fields the household types, never generated. Neither can be exercised end to end without an account, so the record will say what was measured without one and what it cost. | OPEN - operator request, not started | - | Write both templates; bench then box, as for the torrent pair | CC | ## App updates — 4 rows (P3 4) @@ -263,10 +262,11 @@ stopping line that lies. | **R-989** | Business & legal | P4 | **Operator: read `documentation/audits/selfhst-survey-2026-10-12/FELHOM.md` §6 and rule on each recommendation.** READ-ONLY research 2026-10-12: the selfh.st self-hosting survey 2023–2025 (1,869 / 3,655 / 4,081 respondents, hobby users) against our catalog, features, pitch and hardware. Ten ranked recommendations, none acted on; the top three: a supported-hardware page (none exists — finding, §4), off-site backup as a *why* reason in the pitch (only 25.2 % of the crowd backs up off-site), Syncthing as the largest catalog gap (25.9 %). Tables: `TABLE.md`; method and limits: `METHOD.md`. | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages (the 2026 survey replaces it) | operator | | **R-990** | Business & legal | P4 | **Operator: read `documentation/audits/forum-research-2026-10-12/FELHOM.md` §5 and rule on each recommendation.** READ-ONLY research 2026-10-12: three years (#7943–#11092, 2,908 posts, 195 people, 82 beginner threads) of the public PROHARDVER! topic „Home server / házi szerver építése”, tagged and counted by person (`FINDINGS.md`, method and spot-check in `METHOD.md`; no username or post text in git). Ten ranked recommendations, none acted on; the top three: a „which machine works” page (8 of 33 beginners who name their hardware own only a Synology, a router disk or a Raspberry Pi — the installer cannot run there; the same gap is R-989's first recommendation from a different source, and the shipped-page half is R-991), „you do not choose an operating system — we do” (the biggest beginner question, 20 of 82 threads), and „bring your old office PC” as the cheap entry. Catalog gap (≥ 3 people): a DLNA server (7), an ad/DNS blocker (5), a camera recorder (5). | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages | operator | -## Process & tooling — 19 rows (P3 6, P4 13) +## Process & tooling — 20 rows (P3 7, P4 13) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| +| **R-992** | Process & tooling | P3 | **The drill-catalog route that `09` §6.5 documents cannot be followed: `admin/app-catalog-drill` is PRIVATE and no box has catalog credentials.** Measured 2026-10-12 while publishing the Usenet pair: the box's clone fails with `fatal: could not read Username for 'https://gitea.dooplex.hu'` (exit 128), the Gitea API reports the drill repo `private=true` and the live catalog `private=false`, and NEITHER the current controller.yaml on 9202 NOR any of its 30 saved copies has ever carried `git.username` or `git.token` (the controller supports both — `internal/config/config.go` GitConfig). The route worked for the campaigns of 2026-10-10 and the torrent pair earlier the same day, so the repository's visibility changed between then and now. **Worked around, not fixed:** that campaign staged its two templates in the box's own catalog cache (`audits/usenet-2026-10-12/tools/stage_in_cache.py`), which is where the sync copies a catalog change anyway, with the sync interval raised to 12h for the duration and rescan used in place of sync. The real fix is an operator decision: either a read-only catalog credential on the scratch box (a secret on a Tier-0 guest) or making the drill repository readable. Until then every new-app campaign hits this. | WAITING-ON-OPERATOR | the operator's choice between a box credential and a readable drill repo | Decide; then either set `git.username`/`git.token` on 9202 from the credentials store, or flip the drill repo, and correct `09` §6.5 either way | operator | | **R-987** | Process & tooling | P4 | **The 53 catalog app images have never been scanned for known vulnerabilities.** Fix: a monthly trivy pass over the images the catalog pins, reported, not gating. | OPEN | — | Add to the monthly re-test | CC | | **R-986** | Process & tooling | P3 | **No dependency or secret scanner runs on any push, so a known-vulnerable library or a committed key is found only by a review like this one.** Proposed gate: govulncheck (called vulns only), gitleaks on the diff, trivy on our own images; start as WARN with an accepted-hits file, then FAIL. Tools and pinned versions: `audits/security-review-2026-10-12/SCANNERS.md`. Note: gitleaks' default rules did not find R-908's key. | OPEN | — | Build the gate, warn-first | CC | | **R-733** | Process & tooling | P3 | **[P3-LOW] The test bench has NO swap and the boxes have 512 MiB — so a box proof can pass on swap where the bench fails, and nobody records whether a customer guest has swap.** MEASURED 2026-09-30 (R-732): immich's first start was OOM-killed 61–104 times on the bench (swap 0) and passed on 9202 by swapping ~108 MB; the bench given 512 MiB swap passed too. demo-hp 9201, 9202 and demo-felhom 9201 all read `swap: 512`; the golden's guest config is not recorded in its bake evidence, so a customer guest's swap is NOT measured. The harness's memory watch judges `anon` against the limit and never reads `memory.swap.current`. **Needs:** the golden's `swap` read and recorded; the box walk and the harness report `memory.swap.peak` beside `anon`; a decision whether proofs run with swap off (the stricter venue, as R-732's fix was proven). | **READY — rank P3-LOW; owner: CC (harness + golden evidence)** **2026-10-06 night: NARROWED** — the harness records each container's `swap_peak` and the venue's swap (catalog branch `night-held-2026-10-06`, `3f4611c`; SwapRecorded red-proved; bench SwapTotal 0 kB, 9202 524288 kB). LEFT: the golden's swap in its bake evidence, the box walk's record, and whether proofs run with swap off. | — | — | CC | diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index ecf29803..14c825e1 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -1,4 +1,24 @@ -## 2026-10-12 (night) — two download clients on the app page, and what they cost behind a tunnel +## 2026-10-12 (late night) — the Usenet pair on the app page, and what a subscription means + +The catalog gained SABnzbd and NZBGet (`app-catalog-felhom.eu` CHANGELOG, same day, R-988). The +website follows them. + +- **Two new cards** in the media category on `alkalmazasok.html` and `en/apps.html`, each with the + project's own logo (`assets/sabnzbd-logo.svg`, `assets/nzbget-logo.svg`): SABnzbd as the full + client, NZBGet as the light one. +- **Each card says, in bold, that a PAID Usenet subscription is the household's own** and that + without one the app starts but downloads nothing. That is the honest shape: unlike every other app + in the catalog, these two are useless without a service nobody here provides. +- **Neither needs an inbound port**, and the cards say that too — for the Usenet pair the box's closed + ports cost nothing at all, unlike the torrent pair published the same day. +- **The published count moves 60 → 62** on every page that states it, both language sets. +- **The category badges are derived from the cards now, not maintained by hand.** They had drifted: + `media` read 15 with 17 cards in it (the torrent pair was added to the cards and not to the badge) + and `otthon`/`home` read 14 with 16. Both corrected, and the Usenet pair counted. +- `documentation/audits/usenet-2026-10-12/` holds the campaign: both bench runs, both box steps, the + nine defects with their measurements, what a missing provider left unmeasured, and the teardown. + +## 2026-10-12 (night) — two download clients on the app page, and what they cost behind a tunnel The catalog gained qBittorrent and Transmission (`app-catalog-felhom.eu` CHANGELOG, same day). The website follows them. diff --git a/website/alkalmazasok.html b/website/alkalmazasok.html index c2f99365..f62d5bd8 100644 --- a/website/alkalmazasok.html +++ b/website/alkalmazasok.html @@ -4,8 +4,8 @@ <script>document.documentElement.className=document.documentElement.className.replace('no-js','js');</script> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Telepíthető alkalmazások — 60 app otthoni szerverre | Felhom.eu - + Telepíthető alkalmazások — 62 app otthoni szerverre | Felhom.eu + @@ -16,14 +16,14 @@ - + - + @@ -101,7 +101,7 @@ -

Válassz 60 alkalmazás közül. Szinte mind ingyenes, nyílt forráskódú szoftver, és mind a saját szervereden fut.

+

Válassz 62 alkalmazás közül. Szinte mind ingyenes, nyílt forráskódú szoftver, és mind a saját szervereden fut.

Fontos tudnivalók

    @@ -264,7 +264,7 @@

    Média & Szórakozás

    - 15 alkalmazás + 19 alkalmazás
    @@ -422,6 +422,36 @@ Open Source
+ +
+
+ +

SABnzbd

Usenet letöltő
+
+
+

Usenet letöltő teljes felülettel: sorok, kategóriák, automatikus javítás és kicsomagolás. A Radarr és a Sonarr ide is átadhatja a munkát. Fizetős Usenet előfizetés kell hozzá, amit neked kell hoznod – az alkalmazás ennélkül elindul, de letölteni nem tud.

+
Előfizetés kellSaját meghajtóraAutomatikus kicsomagolásRadarr/Sonarr
+
+ +
+ +
+
+ +

NZBGet

Egyszerű Usenet letöltő
+
+
+

Ugyanaz a munka, egyszerűbb felülettel és töredék erőforrásból: mérve 6 MB memória. Fizetős Usenet előfizetés kell hozzá ehhez is. Befelől irányuló portot egyik Usenet letöltő sem igényel, tehát a doboz zárt portjai itt semmit nem vesznek el.

+
Előfizetés kellNagyon kevés erőforrásSaját meghajtóraRadarr/Sonarr
+
+ +
@@ -570,7 +600,7 @@

Otthon & Életmód

- 14 alkalmazás + 16 alkalmazás
diff --git a/website/assets/nzbget-logo.svg b/website/assets/nzbget-logo.svg new file mode 100644 index 00000000..04218752 --- /dev/null +++ b/website/assets/nzbget-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/assets/sabnzbd-logo.svg b/website/assets/sabnzbd-logo.svg new file mode 100644 index 00000000..a1f9dd2f --- /dev/null +++ b/website/assets/sabnzbd-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/en/apps.html b/website/en/apps.html index cdbefc0c..b6eddd2d 100644 --- a/website/en/apps.html +++ b/website/en/apps.html @@ -4,8 +4,8 @@ - Apps to install — 60 apps for a home server | Felhom.eu - + Apps to install — 62 apps for a home server | Felhom.eu + @@ -16,14 +16,14 @@ - + - + @@ -101,7 +101,7 @@ -

Choose from 60 apps. Almost all of them are free, open-source software, and every one runs on your own server.

+

Choose from 62 apps. Almost all of them are free, open-source software, and every one runs on your own server.

Good to know

    @@ -264,7 +264,7 @@

    Media & Entertainment

    - 15 apps + 19 apps
    @@ -422,6 +422,36 @@ Open Source
+ +
+
+ +

SABnzbd

Usenet downloader
+
+
+

A Usenet downloader with the full interface: queues, categories, automatic repair and unpacking. Radarr and Sonarr can hand the job to it too. It needs a paid Usenet subscription, which you bring yourself – without one the app starts, but nothing will download.

+
Subscription neededTo your own driveAutomatic unpackingRadarr/Sonarr
+
+ +
+ +
+
+ +

NZBGet

A simple Usenet downloader
+
+
+

The same job with a simpler interface and a fraction of the resources: 6 MB of memory, measured. It needs a paid Usenet subscription as well. Neither Usenet downloader needs an inbound port, so the box's closed ports cost you nothing here.

+
Subscription neededVery lightTo your own driveRadarr/Sonarr
+
+ +
@@ -570,7 +600,7 @@

Home & Lifestyle

- 14 apps + 16 apps
diff --git a/website/en/faq.html b/website/en/faq.html index 96540397..c6b4577b 100644 --- a/website/en/faq.html +++ b/website/en/faq.html @@ -61,7 +61,7 @@ "name": "How is Felhom.eu different from a Synology or QNAP NAS?", "acceptedAnswer": { "@type": "Answer", - "text": "Synology and QNAP make NAS (network storage) devices built mainly for storing files, with their own closed software. I offer an open, Docker-based system instead, where the catalog's apps install in a few clicks – from a photo manager to a media server to a password manager. A NAS offers a limited choice of apps and depends on the maker's software updates. With Felhom you choose from 60 apps, and if something is missing, I look at whether it can be added." + "text": "Synology and QNAP make NAS (network storage) devices built mainly for storing files, with their own closed software. I offer an open, Docker-based system instead, where the catalog's apps install in a few clicks – from a photo manager to a media server to a password manager. A NAS offers a limited choice of apps and depends on the maker's software updates. With Felhom you choose from 62 apps, and if something is missing, I look at whether it can be added." } }, { @@ -133,7 +133,7 @@ "name": "Which apps can I run?", "acceptedAnswer": { "@type": "Answer", - "text": "I offer 60 apps – the full list is on the Apps page. A few examples: Immich – a Google Photos alternative that backs up photos and videos automatically. Jellyfin/Plex – your own Netflix for films and series. Nextcloud – store and share files (instead of Google Drive). Vaultwarden – a password manager for the whole family. Paperless-ngx – scan documents and make them searchable. If something is not on the list, ask – I will look at whether it can join the catalog." + "text": "I offer 62 apps – the full list is on the Apps page. A few examples: Immich – a Google Photos alternative that backs up photos and videos automatically. Jellyfin/Plex – your own Netflix for films and series. Nextcloud – store and share files (instead of Google Drive). Vaultwarden – a password manager for the whole family. Paperless-ngx – scan documents and make them searchable. If something is not on the list, ask – I will look at whether it can join the catalog." } }, { @@ -491,7 +491,7 @@

Synology and QNAP make NAS (network storage) devices built mainly for storing files, with their own closed software. I offer an open, Docker-based system instead, where the catalog's apps install in a few clicks – from a photo manager to a media server to a password manager.

-

A NAS offers a limited choice of apps and depends on the maker's software updates. With Felhom you choose from 60 apps, and if something is missing, I look at whether it can be added.

+

A NAS offers a limited choice of apps and depends on the maker's software updates. With Felhom you choose from 62 apps, and if something is missing, I look at whether it can be added.

@@ -607,7 +607,7 @@
-

I offer 60 apps – the full list is on the Apps page. A few examples:

+

I offer 62 apps – the full list is on the Apps page. A few examples:

Immich – a Google Photos alternative that backs up photos and videos automatically. Jellyfin/Plex – your own Netflix for films and series. Nextcloud – store and share files (instead of Google Drive). Vaultwarden – a password manager for the whole family. Paperless-ngx – scan documents and make them searchable.

If something is not on the list, ask – I will look at whether it can join the catalog.

diff --git a/website/en/index.html b/website/en/index.html index c3544377..b3c1f1d0 100644 --- a/website/en/index.html +++ b/website/en/index.html @@ -5,7 +5,7 @@ - + Felhom.eu — Your own cloud, your own rules | A home server service in Hungary @@ -139,7 +139,7 @@
  • Photos, documents, media
  • Network shares
  • -
  • 60 apps to install
  • +
  • 62 apps to install
  • @@ -245,7 +245,7 @@

    What you can install

    -

    60 apps — almost all of them open source

    +

    62 apps — almost all of them open source

    See all applications →
    diff --git a/website/en/services.html b/website/en/services.html index b21e5757..ba235806 100644 --- a/website/en/services.html +++ b/website/en/services.html @@ -110,7 +110,7 @@

    The box's folders appear in Windows File Explorer and the Mac Finder — you drag files onto them like onto any other folder. Each share can be set to read-only.

  • -

    60 apps to install

    +

    62 apps to install

    You install them from the Dashboard in a few clicks — no command line. Almost all of them are open source (the exceptions are marked on the apps page), and every one runs on your machine.

  • diff --git a/website/en/technology.html b/website/en/technology.html index 4358bf17..59da2340 100644 --- a/website/en/technology.html +++ b/website/en/technology.html @@ -178,7 +178,7 @@
    • A web interface in English or Hungarian – the household chooses, and no command-line knowledge is needed
    • -
    • App catalog – 60 apps to install in a few clicks
    • +
    • App catalog – 62 apps to install in a few clicks
    • Resource view – memory and storage use as it happens
    • Automatic backups – databases and files, with their status watched
    • App updates – only versions we have tried, a fresh backup first, and if the new version does not start, the box goes back to the previous one by itself
    • diff --git a/website/gyik.html b/website/gyik.html index 4cd0d378..31f48029 100644 --- a/website/gyik.html +++ b/website/gyik.html @@ -61,7 +61,7 @@ "name": "Mi a különbség a Felhom.eu és a Synology/QNAP NAS szolgáltatásai között?", "acceptedAnswer": { "@type": "Answer", - "text": "A Synology és QNAP elsősorban fájltárolásra készült NAS (hálózati adattároló) eszközök, saját zárt szoftverrel. Én ezzel szemben egy nyílt, Docker-alapú rendszert kínálok, amelyre a katalógus alkalmazásai pár kattintással telepíthetők – fotókezelőtől a média szerveren át a jelszókezelőig. A NAS-ok korlátozott alkalmazás-választékot kínálnak és a gyártó szoftverfrissítéseitől függenek. A Felhomnál 60 alkalmazásból válogathatsz, és ha valami hiányzik, megnézem, felvehető-e." + "text": "A Synology és QNAP elsősorban fájltárolásra készült NAS (hálózati adattároló) eszközök, saját zárt szoftverrel. Én ezzel szemben egy nyílt, Docker-alapú rendszert kínálok, amelyre a katalógus alkalmazásai pár kattintással telepíthetők – fotókezelőtől a média szerveren át a jelszókezelőig. A NAS-ok korlátozott alkalmazás-választékot kínálnak és a gyártó szoftverfrissítéseitől függenek. A Felhomnál 62 alkalmazásból válogathatsz, és ha valami hiányzik, megnézem, felvehető-e." } }, { @@ -133,7 +133,7 @@ "name": "Milyen alkalmazásokat tudok futtatni?", "acceptedAnswer": { "@type": "Answer", - "text": "60 alkalmazást kínálok – a teljes listát az Alkalmazások oldalon találod. Néhány példa: Immich – Google Photos alternatíva fotók és videók automatikus mentésével. Jellyfin/Plex – saját Netflix filmekhez és sorozatokhoz. Nextcloud – fájlok tárolása és megosztása (Google Drive helyett). Vaultwarden – jelszókezelő az egész családnak. Paperless-ngx – dokumentumok digitalizálása és kereshetővé tétele. Ha valami nincs a listán, kérdezz – megnézem, felvehető-e a katalógusba." + "text": "62 alkalmazást kínálok – a teljes listát az Alkalmazások oldalon találod. Néhány példa: Immich – Google Photos alternatíva fotók és videók automatikus mentésével. Jellyfin/Plex – saját Netflix filmekhez és sorozatokhoz. Nextcloud – fájlok tárolása és megosztása (Google Drive helyett). Vaultwarden – jelszókezelő az egész családnak. Paperless-ngx – dokumentumok digitalizálása és kereshetővé tétele. Ha valami nincs a listán, kérdezz – megnézem, felvehető-e a katalógusba." } }, { @@ -491,7 +491,7 @@

      A Synology és QNAP elsősorban fájltárolásra készült NAS (hálózati adattároló) eszközök, saját zárt szoftverrel. Én ezzel szemben egy nyílt, Docker-alapú rendszert kínálok, amelyre a katalógus alkalmazásai pár kattintással telepíthetők – fotókezelőtől a média szerveren át a jelszókezelőig.

      -

      A NAS-ok korlátozott alkalmazás-választékot kínálnak és a gyártó szoftverfrissítéseitől függenek. A Felhomnál 60 alkalmazásból válogathatsz, és ha valami hiányzik, megnézem, felvehető-e.

      +

      A NAS-ok korlátozott alkalmazás-választékot kínálnak és a gyártó szoftverfrissítéseitől függenek. A Felhomnál 62 alkalmazásból válogathatsz, és ha valami hiányzik, megnézem, felvehető-e.

  • @@ -607,7 +607,7 @@
    -

    60 alkalmazást kínálok – a teljes listát az Alkalmazások oldalon találod. Néhány példa:

    +

    62 alkalmazást kínálok – a teljes listát az Alkalmazások oldalon találod. Néhány példa:

    Immich – Google Photos alternatíva fotók és videók automatikus mentésével. Jellyfin/Plex – saját Netflix filmekhez és sorozatokhoz. Nextcloud – fájlok tárolása és megosztása (Google Drive helyett). Vaultwarden – jelszókezelő az egész családnak. Paperless-ngx – dokumentumok digitalizálása és kereshetővé tétele.

    Ha valami nincs a listán, kérdezz – megnézem, felvehető-e a katalógusba.

    diff --git a/website/index.html b/website/index.html index 04d1c2bd..3946103f 100644 --- a/website/index.html +++ b/website/index.html @@ -245,7 +245,7 @@

    Amit telepíthetsz

    -

    Fájlkezelés, fotók és média, dokumentumok, jelszókezelés, okosotthon, fejlesztői eszközök. Nextcloud, Immich, Jellyfin, Paperless-ngx, Vaultwarden, Home Assistant és még 54 másik: 60 alkalmazás, szinte mind nyílt forráskódú.

    +

    Fájlkezelés, fotók és média, dokumentumok, jelszókezelés, okosotthon, fejlesztői eszközök. Nextcloud, Immich, Jellyfin, Paperless-ngx, Vaultwarden, Home Assistant és még 54 másik: 62 alkalmazás, szinte mind nyílt forráskódú.

    Összes alkalmazás megtekintése →
    diff --git a/website/technologiak.html b/website/technologiak.html index 677c43ab..a15f741e 100644 --- a/website/technologiak.html +++ b/website/technologiak.html @@ -178,7 +178,7 @@
    • Magyar vagy angol webes felület – a háztartás választ, és nincs szükség parancssor ismeretre
    • -
    • Alkalmazás katalógus – 60 app telepíthető pár kattintással
    • +
    • Alkalmazás katalógus – 62 app telepíthető pár kattintással
    • Erőforrás figyelés – memória és tárhely használat valós időben
    • Automatikus mentések – adatbázis és fájl backup, állapotfigyeléssel
    • Alkalmazás frissítések – csak kipróbált verzió, előtte friss mentés, és ha az új verzió nem indul, a doboz magától visszaáll az előzőre