youpdated 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- youpdated-0.2.0/CHANGELOG.md +111 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/PKG-INFO +107 -4
- {youpdated-0.1.0 → youpdated-0.2.0}/README.md +103 -3
- {youpdated-0.1.0 → youpdated-0.2.0}/SECURITY.md +19 -1
- {youpdated-0.1.0 → youpdated-0.2.0}/pyproject.toml +3 -2
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/__init__.py +1 -1
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/cli.py +166 -10
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/config.py +18 -2
- youpdated-0.2.0/youpdated/crypto.py +216 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/http.py +50 -3
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/browser.py +17 -10
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/state.py +51 -2
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated.egg-info/PKG-INFO +107 -4
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated.egg-info/SOURCES.txt +1 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated.egg-info/requires.txt +4 -0
- youpdated-0.1.0/CHANGELOG.md +0 -36
- {youpdated-0.1.0 → youpdated-0.2.0}/ADDING_SOURCES.md +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/CONTRIBUTING.md +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/LICENSE +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/MANIFEST.in +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/setup.cfg +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/__main__.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/cleanup.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/models.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/py.typed +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/registry.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/render/__init__.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/render/json_out.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/render/rss_out.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/render/terminal.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/runner.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/__init__.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/base.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/feed.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/generic.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/github.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/itch.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/npm.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/steam.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated/sources/youtube.py +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated.egg-info/dependency_links.txt +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated.egg-info/entry_points.txt +0 -0
- {youpdated-0.1.0 → youpdated-0.2.0}/youpdated.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] — 2026-08-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Encryption at rest for the config and history** ([#5](https://github.com/Void1-1/youpdated/issues/5)).
|
|
12
|
+
`youpdated encrypt` (alias `set-encrypted`) converts an existing setup in place; `youpdated decrypt`
|
|
13
|
+
converts it back. Every command detects an encrypted setup on its own and asks for the passphrase
|
|
14
|
+
once, or reads it from `YOUPDATED_PASSPHRASE` for unattended runs.
|
|
15
|
+
|
|
16
|
+
Files are encrypted whole with AES-256-GCM under a scrypt-derived key (n=2¹⁶, r=8, p=1), with the
|
|
17
|
+
KDF parameters authenticated as additional data so they cannot be downgraded. Decryption happens
|
|
18
|
+
**in memory**: the config is parsed from a decrypted buffer and the SQLite database is
|
|
19
|
+
deserialized into an in-memory database and written back encrypted when the run ends, so no
|
|
20
|
+
plaintext copy is put on disk even mid-run. A read-only run leaves the file byte-for-byte alone.
|
|
21
|
+
|
|
22
|
+
Needs the `cryptography` package: `pip install 'youpdated[encryption]'`. Installs without it
|
|
23
|
+
behave exactly as before.
|
|
24
|
+
|
|
25
|
+
- **`youpdated init --encrypt`** writes the starter config already encrypted, so a setup that is
|
|
26
|
+
meant to be private never has a plaintext config on disk at all — unlike `init` then `encrypt`,
|
|
27
|
+
which leaves the original blocks in free space.
|
|
28
|
+
|
|
29
|
+
- **A proxy preflight.** When `privacy.proxy` is set, the proxy is checked before the run starts
|
|
30
|
+
and the run is refused with exit `1` if it is unreachable. `--test` reports it instead of
|
|
31
|
+
aborting.
|
|
32
|
+
|
|
33
|
+
- **`youpdated.crypto` is a documented standalone module.** `from youpdated import crypto` gives
|
|
34
|
+
you the container directly; everything in its `__all__` is a supported surface. It stays an
|
|
35
|
+
optional *dependency* rather than a separate distribution on purpose: nothing in it imports
|
|
36
|
+
`cryptography` at module scope, so a plain install already pays nothing for it, and shipping the
|
|
37
|
+
container format apart from the code that reads it would risk version skew on files that are the
|
|
38
|
+
user's only copy.
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **A dead proxy now stops the run instead of failing every target.** Requests already failed
|
|
43
|
+
closed (httpx routes everything through the proxy and never falls back to a direct connection)
|
|
44
|
+
but with Tor off, a run would fail each target separately, record an empty baseline, and still
|
|
45
|
+
exit `0`. In a cron log that is indistinguishable from "nothing new". It now exits `1` before the
|
|
46
|
+
state database is even opened, so nothing is recorded.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- **A broken SOCKS handshake escaped the retry path.** `socksio` raises `ProtocolError`, which is
|
|
51
|
+
not an `httpx.HTTPError`, so a proxy port answering with something that is not SOCKS5 bypassed
|
|
52
|
+
the retries and surfaced as a raw exception rather than a `FetchError`. The client now treats
|
|
53
|
+
`SOCKSError` as a network error like any other.
|
|
54
|
+
|
|
55
|
+
- **`tests/test_cleanup.py` could delete a real `./youpdated.yaml`.** The fixture redirected the
|
|
56
|
+
config and data directories but not the working directory, so `find_traces()` picked up the
|
|
57
|
+
project config of whoever ran the suite from a directory that had one, and `remove_traces()`
|
|
58
|
+
deleted it. The fixture now chdirs to the temp directory.
|
|
59
|
+
|
|
60
|
+
## [0.1.1] — 2026-08-18
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- **`browser` / Brave: a server error killed the target instead of falling back.** The Brave source
|
|
65
|
+
reads the GitHub REST API and keeps the `.atom` feed as a fallback, but only a 403 or 429 reached
|
|
66
|
+
it. A 5xx: a timeout, or a DNS failure, was retried, then raised, and the whole target was
|
|
67
|
+
reported as failed. Observed against `api.github.com` returning 504. Any failure the HTTP client
|
|
68
|
+
gives up on now falls back to the atom feed; a genuine outage of *both* still reports an error.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- Retry backoff is configurable on the HTTP client (`retry_backoff`), so the test suite no longer spends real seconds exercising retry paths. The suite went from ~4.9s to ~0.4s.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- A `release` workflow that publishes to PyPI via trusted publishing when a GitHub Release is
|
|
77
|
+
published, gated on the full 13-job test matrix and on the tag matching the version in
|
|
78
|
+
`pyproject.toml`.
|
|
79
|
+
|
|
80
|
+
## [0.1.0] — 2026-08-18
|
|
81
|
+
|
|
82
|
+
First release.
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- **Seven sources**, all working without accounts or API keys:
|
|
87
|
+
- `github` — releases, tags, and commits via `.atom` feeds
|
|
88
|
+
- `npm` — newly published versions from public registry
|
|
89
|
+
- `steam` — patch notes and news; resolves the store name from a bare appid
|
|
90
|
+
- `itch` — devlog posts and new builds, fingerprinted from the game page
|
|
91
|
+
- `youtube` — channels and playlists, with Invidious and Data API fallback
|
|
92
|
+
- `browser` — Chrome, Brave, Firefox, and Edge releases across platforms and channels
|
|
93
|
+
- `feed` — any RSS/Atom URL, for apps without a dedicated source
|
|
94
|
+
- **Plugin architecture**: sources register in-tree with `@register` or ship from a third-party package through a `youpdated.sources` entry point.
|
|
95
|
+
- **Config**: every source entry takes a bare value for the common case or a
|
|
96
|
+
mapping for advanced options
|
|
97
|
+
- **Incremental reporting**: a SQLite history so each run reports what changed. First run records a baseline.
|
|
98
|
+
- **Three output formats**: terminal report, `--json`, and `--rss`
|
|
99
|
+
- **Privacy controls**: optional SOCKS/HTTP proxy covering every request, user-agent rotation, per-host request pacing with jitter, per-request cookie clearing, and conditional GETs. `--test` prints URLs without sending
|
|
100
|
+
- **`youpdated uninstall`** to remove every file from the tool. Refuses directories it didn't create or have other files.
|
|
101
|
+
|
|
102
|
+
### Known issues
|
|
103
|
+
|
|
104
|
+
- YouTube's RSS endpoint throttles occassionaly and 404s valid URLs; fallback covers, but a run can still fail all three. Retry or set `privacy.proxy`.
|
|
105
|
+
- Some itch games publish no "Updated" timestamp, so build updates are reported undated.
|
|
106
|
+
- Firefox publishes current versions, so it reports one item per channel.
|
|
107
|
+
- Edge exposes release notes only for the stable and beta channels. (But like, it's Edge, why do you want to know when it updates?)
|
|
108
|
+
|
|
109
|
+
[0.2.0]: https://github.com/Void1-1/youpdated/releases/tag/v0.2.0
|
|
110
|
+
[0.1.1]: https://github.com/Void1-1/youpdated/releases/tag/v0.1.1
|
|
111
|
+
[0.1.0]: https://github.com/Void1-1/youpdated/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: youpdated
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Simple update tracker for games, apps, and packages
|
|
5
5
|
Author-email: Void1-1 <161782542+Void1-1@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,9 +32,12 @@ Requires-Dist: feedparser>=6.0
|
|
|
32
32
|
Requires-Dist: rich>=13.0
|
|
33
33
|
Requires-Dist: PyYAML>=6.0
|
|
34
34
|
Requires-Dist: platformdirs>=4.0
|
|
35
|
+
Provides-Extra: encryption
|
|
36
|
+
Requires-Dist: cryptography>=42.0; extra == "encryption"
|
|
35
37
|
Provides-Extra: dev
|
|
36
38
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
37
39
|
Requires-Dist: respx>=0.21; extra == "dev"
|
|
40
|
+
Requires-Dist: cryptography>=42.0; extra == "dev"
|
|
38
41
|
Provides-Extra: publish
|
|
39
42
|
Requires-Dist: build>=1.2; extra == "publish"
|
|
40
43
|
Requires-Dist: twine>=5.0; extra == "publish"
|
|
@@ -99,7 +102,7 @@ it on your `PATH`:
|
|
|
99
102
|
|
|
100
103
|
```sh
|
|
101
104
|
export PATH="$PWD/.venv/bin:$PATH" # macOS / Linux
|
|
102
|
-
youpdated --version # -> youpdated 0.
|
|
105
|
+
youpdated --version # -> youpdated 0.2.0
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
```powershell
|
|
@@ -261,6 +264,9 @@ youpdated check -v # show each request, its status, and item
|
|
|
261
264
|
youpdated check --state ./test.db # use a throwaway history file
|
|
262
265
|
youpdated sources # list available sources
|
|
263
266
|
youpdated init --force # rewrite the starter config
|
|
267
|
+
youpdated init --encrypt # starter config written encrypted from the start
|
|
268
|
+
youpdated encrypt # lock the config and history behind a passphrase
|
|
269
|
+
youpdated decrypt # convert them back to plain files
|
|
264
270
|
youpdated uninstall --test # list every file the tool wrote
|
|
265
271
|
```
|
|
266
272
|
|
|
@@ -272,7 +278,7 @@ Running `youpdated` with no arguments runs `youpdated check`.
|
|
|
272
278
|
youpdated check --json | jq '.updates[] | select(.source == "github") | .title'
|
|
273
279
|
```
|
|
274
280
|
|
|
275
|
-
Exit codes: `0` success, `1` config error, `2` with `--fail-on-error` when a source failed, `130` on interrupt.
|
|
281
|
+
Exit codes: `0` success, `1` config, encryption, or proxy error, `2` with `--fail-on-error` when a source failed, `130` on interrupt.
|
|
276
282
|
|
|
277
283
|
## Running it on schedule
|
|
278
284
|
|
|
@@ -323,6 +329,12 @@ Once a day is plenty, most of these sources change slowly, and conditional reque
|
|
|
323
329
|
| Everything reports as new again | The history database was deleted or `--state` points somewhere new. |
|
|
324
330
|
| `youpdated: command not found` | The virtualenv isn't on your `PATH`. Use `.venv/bin/youpdated`. |
|
|
325
331
|
| `ModuleNotFoundError: youpdated` | You used `pip install -e .` on MacOS |
|
|
332
|
+
| `Encryption error: wrong passphrase, or the file has been modified` | The passphrase doesn't match, or the file was edited or truncated. Nothing is written on a failed unlock. |
|
|
333
|
+
| `Encryption error: ... no terminal to ask on` | A cron or piped run can't prompt. Set `YOUPDATED_PASSPHRASE`. |
|
|
334
|
+
| `encryption needs the cryptography package` | `pip install 'youpdated[encryption]'`. |
|
|
335
|
+
| `... is not encrypted, but a passphrase was given` | A half-converted setup. Re-run `youpdated encrypt` to finish it. |
|
|
336
|
+
| `Proxy error: cannot reach the proxy at ...` | `privacy.proxy` is set but nothing is listening. Start Tor (or your proxy), or remove the line. Nothing was fetched or recorded. |
|
|
337
|
+
| Every source fails with `ProtocolError: Malformed reply` | Something is listening on the proxy port but isn't a SOCKS5 proxy. Check the port and scheme. |
|
|
326
338
|
|
|
327
339
|
To start over from a clean slate, delete the history database (the path is in the table in [step 2](#2-create-your-config)); your config is untouched.
|
|
328
340
|
|
|
@@ -357,16 +369,107 @@ It prints the command to remove the package itself. (which it can't do while run
|
|
|
357
369
|
|
|
358
370
|
If you installed into a dedicated virtualenv, deleting that directory removes the package too.
|
|
359
371
|
|
|
372
|
+
## Encryption at rest
|
|
373
|
+
|
|
374
|
+
By default the config and the history database are plain files. Anyone with your login, or your unencrypted backups, can read the list. To lock them behind a passphrase:
|
|
375
|
+
|
|
376
|
+
```sh
|
|
377
|
+
pip install 'youpdated[encryption]' # pulls in `cryptography`; not needed otherwise
|
|
378
|
+
youpdated encrypt # `set-encrypted` also works
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Starting fresh, skip the plaintext step entirely. `init --encrypt` writes the starter config already encrypted, so it never exists in the clear at all:
|
|
382
|
+
|
|
383
|
+
```sh
|
|
384
|
+
youpdated init --encrypt
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
You can't edit in place: run `youpdated decrypt`, edit, then `youpdated encrypt` again. That does put the config on disk in plaintext while you work on it, so `init --encrypt` is rarely worth anything, or you edit before encrypting the first time.
|
|
388
|
+
|
|
389
|
+
It asks for a passphrase twice and rewrites both files in place. From then on every command asks for it once, decrypts **into memory**, and writes back encrypted when the run ends.
|
|
390
|
+
|
|
391
|
+
```sh
|
|
392
|
+
youpdated check # Passphrase: ...
|
|
393
|
+
youpdated decrypt # back to plain files
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
For cron and other unattended runs there is no terminal to ask on, so pass the passphrase in the environment instead:
|
|
397
|
+
|
|
398
|
+
```cron
|
|
399
|
+
0 9 * * * YOUPDATED_PASSPHRASE='...' /path/to/.venv/bin/youpdated check >> ~/youpdated.log 2>&1
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
That trades the passphrase's secrecy for the crontab's. Ensure it is more protected than your config was. Reading it from a file your shell sources, or from a keychain helper, is stronger.
|
|
403
|
+
|
|
404
|
+
**What this does and does not protect.** Files are encrypted whole with AES-256-GCM under a key derived from the passphrase with scrypt (n=2¹⁶, r=8), and the tag is checked on every read, so a modified file is refused rather than trusted. It does **not** cover anything while Youpdated is running: the passphrase and the decrypted config are in the process's memory, and `YOUPDATED_PASSPHRASE` is visible to other processes of the same user. Nor does it erase the plaintext that was already on the disk. `encrypt` overwrites the file, but the old blocks may survive in free space until they are reused.
|
|
405
|
+
|
|
406
|
+
**There is no recovery.** Don't forget your passphrase, or you'll have to recreate configs.
|
|
407
|
+
|
|
408
|
+
| | Encrypted | Plain |
|
|
409
|
+
| --- | --- | --- |
|
|
410
|
+
| `youpdated check` | asks for the passphrase | runs |
|
|
411
|
+
| `cat ~/.config/youpdated/config.yaml` | binary | readable |
|
|
412
|
+
| `sqlite3 state.sqlite3 'select * from seen'` | `file is not a database` | readable |
|
|
413
|
+
| File permissions | `0600` on macOS/Linux, see below on Windows | as created |
|
|
414
|
+
|
|
415
|
+
**On Windows the file mode does nothing.** There are no POSIX permission bits, so the `0600` the
|
|
416
|
+
writer asks for is ignored apart from the read-only flag, and access is decided by the ACL the file
|
|
417
|
+
inherits from its directory. In the default location under `%LOCALAPPDATA%` that already excludes
|
|
418
|
+
other standard users. The encryption itself is unaffected and works
|
|
419
|
+
identically on all three platforms.
|
|
420
|
+
|
|
421
|
+
### Using it as a module
|
|
422
|
+
|
|
423
|
+
[youpdated/crypto.py](https://github.com/Void1-1/youpdated/blob/main/youpdated/crypto.py) stands alone.
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
from youpdated import crypto
|
|
427
|
+
|
|
428
|
+
blob = crypto.encrypt(b"anything", "passphrase")
|
|
429
|
+
crypto.write_private("secret.bin", blob) # atomic, 0600
|
|
430
|
+
assert crypto.decrypt(blob, "passphrase") == b"anything"
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Everything in `crypto.__all__` is supported, the container format included: `encrypt`, `decrypt`, `is_encrypted`, `is_encrypted_file`, `write_private`, `prompt_passphrase`, `available`, `require_backend`, `EncryptionError`, `MAGIC`, `VERSION`, `PASSPHRASE_ENV`. A `decrypt` on a file this version can't read raises rather than guessing, and `VERSION` is bumped if the layout ever changes.
|
|
434
|
+
|
|
435
|
+
The same passphrase threads through the two core entry points, so you can drive an encrypted setup without the CLI:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from youpdated.config import load_config
|
|
439
|
+
from youpdated.state import State
|
|
440
|
+
|
|
441
|
+
config = load_config("youpdated.yaml", passphrase="...")
|
|
442
|
+
with State("state.sqlite3", passphrase="...") as state:
|
|
443
|
+
print(state.seen_count())
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```sh
|
|
447
|
+
$ python -c "from youpdated.cli import main; main(['sources'])" && python -c "
|
|
448
|
+
import sys, youpdated.cli
|
|
449
|
+
print('cryptography loaded:', any(m.startswith('cryptography') for m in sys.modules))"
|
|
450
|
+
cryptography loaded: False
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Don't use encryption with untrusted sources or non-official code without verifying security yourself!
|
|
454
|
+
|
|
360
455
|
## Privacy
|
|
361
456
|
|
|
362
457
|
- **No credentials.** Every source works unauthenticated. `GITHUB_TOKEN` and `YOUTUBE_API_KEY` are read from the environment if set (only for raising rate limits) and are never written to config.
|
|
363
|
-
- **Local only.** Config and history live in your platform's config/data directories. Nothing is sent anywhere except the sources you list.
|
|
458
|
+
- **Local only.** Config and history live in your platform's config/data directories. Nothing is sent anywhere except the sources you list. They can be [encrypted at rest](#encryption-at-rest).
|
|
364
459
|
- **One HTTP path.** Every request goes through [youpdated/http.py](https://github.com/Void1-1/youpdated/blob/main/youpdated/http.py). `privacy.proxy` covers all traffic. To check:
|
|
365
460
|
|
|
366
461
|
```sh
|
|
367
462
|
youpdated check --all --no-save # with privacy.proxy: socks5://127.0.0.1:9
|
|
368
463
|
```
|
|
369
464
|
|
|
465
|
+
- **The proxy fails closed.** There is no direct fallback: if `privacy.proxy` is set and the proxy
|
|
466
|
+
is unreachable, requests error rather than going around it. Youpdated also checks the proxy is
|
|
467
|
+
up *before* the run starts and refuses with exit `1` if it isn't, so a run with Tor down can't
|
|
468
|
+
quietly report "nothing new" and record an empty baseline. `--test` reports the proxy as
|
|
469
|
+
unreachable instead of aborting, since it sends nothing either way.
|
|
470
|
+
- **Hostnames are resolved by the proxy, not by you.** SOCKS5 CONNECT sends the domain name to the
|
|
471
|
+
proxy (`ATYP 3`), so your local resolver never sees which sites you watch. A proxied request with a locally-resolved hostname would leak the whole list to your DNS server.
|
|
472
|
+
|
|
370
473
|
- **Cookies are discarded** on every request, so nothing accumulates across a run.
|
|
371
474
|
- **Requests are paced** per host with random jitter instead of arriving in a burst.
|
|
372
475
|
- **Conditional GETs** (ETag / Last-Modified) mean unchanged feeds are re-fetched cheaply, which is both faster and less fingerprintable. `--all` turns them off, since a 304 has no items.
|
|
@@ -57,7 +57,7 @@ it on your `PATH`:
|
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
59
|
export PATH="$PWD/.venv/bin:$PATH" # macOS / Linux
|
|
60
|
-
youpdated --version # -> youpdated 0.
|
|
60
|
+
youpdated --version # -> youpdated 0.2.0
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
```powershell
|
|
@@ -219,6 +219,9 @@ youpdated check -v # show each request, its status, and item
|
|
|
219
219
|
youpdated check --state ./test.db # use a throwaway history file
|
|
220
220
|
youpdated sources # list available sources
|
|
221
221
|
youpdated init --force # rewrite the starter config
|
|
222
|
+
youpdated init --encrypt # starter config written encrypted from the start
|
|
223
|
+
youpdated encrypt # lock the config and history behind a passphrase
|
|
224
|
+
youpdated decrypt # convert them back to plain files
|
|
222
225
|
youpdated uninstall --test # list every file the tool wrote
|
|
223
226
|
```
|
|
224
227
|
|
|
@@ -230,7 +233,7 @@ Running `youpdated` with no arguments runs `youpdated check`.
|
|
|
230
233
|
youpdated check --json | jq '.updates[] | select(.source == "github") | .title'
|
|
231
234
|
```
|
|
232
235
|
|
|
233
|
-
Exit codes: `0` success, `1` config error, `2` with `--fail-on-error` when a source failed, `130` on interrupt.
|
|
236
|
+
Exit codes: `0` success, `1` config, encryption, or proxy error, `2` with `--fail-on-error` when a source failed, `130` on interrupt.
|
|
234
237
|
|
|
235
238
|
## Running it on schedule
|
|
236
239
|
|
|
@@ -281,6 +284,12 @@ Once a day is plenty, most of these sources change slowly, and conditional reque
|
|
|
281
284
|
| Everything reports as new again | The history database was deleted or `--state` points somewhere new. |
|
|
282
285
|
| `youpdated: command not found` | The virtualenv isn't on your `PATH`. Use `.venv/bin/youpdated`. |
|
|
283
286
|
| `ModuleNotFoundError: youpdated` | You used `pip install -e .` on MacOS |
|
|
287
|
+
| `Encryption error: wrong passphrase, or the file has been modified` | The passphrase doesn't match, or the file was edited or truncated. Nothing is written on a failed unlock. |
|
|
288
|
+
| `Encryption error: ... no terminal to ask on` | A cron or piped run can't prompt. Set `YOUPDATED_PASSPHRASE`. |
|
|
289
|
+
| `encryption needs the cryptography package` | `pip install 'youpdated[encryption]'`. |
|
|
290
|
+
| `... is not encrypted, but a passphrase was given` | A half-converted setup. Re-run `youpdated encrypt` to finish it. |
|
|
291
|
+
| `Proxy error: cannot reach the proxy at ...` | `privacy.proxy` is set but nothing is listening. Start Tor (or your proxy), or remove the line. Nothing was fetched or recorded. |
|
|
292
|
+
| Every source fails with `ProtocolError: Malformed reply` | Something is listening on the proxy port but isn't a SOCKS5 proxy. Check the port and scheme. |
|
|
284
293
|
|
|
285
294
|
To start over from a clean slate, delete the history database (the path is in the table in [step 2](#2-create-your-config)); your config is untouched.
|
|
286
295
|
|
|
@@ -315,16 +324,107 @@ It prints the command to remove the package itself. (which it can't do while run
|
|
|
315
324
|
|
|
316
325
|
If you installed into a dedicated virtualenv, deleting that directory removes the package too.
|
|
317
326
|
|
|
327
|
+
## Encryption at rest
|
|
328
|
+
|
|
329
|
+
By default the config and the history database are plain files. Anyone with your login, or your unencrypted backups, can read the list. To lock them behind a passphrase:
|
|
330
|
+
|
|
331
|
+
```sh
|
|
332
|
+
pip install 'youpdated[encryption]' # pulls in `cryptography`; not needed otherwise
|
|
333
|
+
youpdated encrypt # `set-encrypted` also works
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Starting fresh, skip the plaintext step entirely. `init --encrypt` writes the starter config already encrypted, so it never exists in the clear at all:
|
|
337
|
+
|
|
338
|
+
```sh
|
|
339
|
+
youpdated init --encrypt
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
You can't edit in place: run `youpdated decrypt`, edit, then `youpdated encrypt` again. That does put the config on disk in plaintext while you work on it, so `init --encrypt` is rarely worth anything, or you edit before encrypting the first time.
|
|
343
|
+
|
|
344
|
+
It asks for a passphrase twice and rewrites both files in place. From then on every command asks for it once, decrypts **into memory**, and writes back encrypted when the run ends.
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
youpdated check # Passphrase: ...
|
|
348
|
+
youpdated decrypt # back to plain files
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
For cron and other unattended runs there is no terminal to ask on, so pass the passphrase in the environment instead:
|
|
352
|
+
|
|
353
|
+
```cron
|
|
354
|
+
0 9 * * * YOUPDATED_PASSPHRASE='...' /path/to/.venv/bin/youpdated check >> ~/youpdated.log 2>&1
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
That trades the passphrase's secrecy for the crontab's. Ensure it is more protected than your config was. Reading it from a file your shell sources, or from a keychain helper, is stronger.
|
|
358
|
+
|
|
359
|
+
**What this does and does not protect.** Files are encrypted whole with AES-256-GCM under a key derived from the passphrase with scrypt (n=2¹⁶, r=8), and the tag is checked on every read, so a modified file is refused rather than trusted. It does **not** cover anything while Youpdated is running: the passphrase and the decrypted config are in the process's memory, and `YOUPDATED_PASSPHRASE` is visible to other processes of the same user. Nor does it erase the plaintext that was already on the disk. `encrypt` overwrites the file, but the old blocks may survive in free space until they are reused.
|
|
360
|
+
|
|
361
|
+
**There is no recovery.** Don't forget your passphrase, or you'll have to recreate configs.
|
|
362
|
+
|
|
363
|
+
| | Encrypted | Plain |
|
|
364
|
+
| --- | --- | --- |
|
|
365
|
+
| `youpdated check` | asks for the passphrase | runs |
|
|
366
|
+
| `cat ~/.config/youpdated/config.yaml` | binary | readable |
|
|
367
|
+
| `sqlite3 state.sqlite3 'select * from seen'` | `file is not a database` | readable |
|
|
368
|
+
| File permissions | `0600` on macOS/Linux, see below on Windows | as created |
|
|
369
|
+
|
|
370
|
+
**On Windows the file mode does nothing.** There are no POSIX permission bits, so the `0600` the
|
|
371
|
+
writer asks for is ignored apart from the read-only flag, and access is decided by the ACL the file
|
|
372
|
+
inherits from its directory. In the default location under `%LOCALAPPDATA%` that already excludes
|
|
373
|
+
other standard users. The encryption itself is unaffected and works
|
|
374
|
+
identically on all three platforms.
|
|
375
|
+
|
|
376
|
+
### Using it as a module
|
|
377
|
+
|
|
378
|
+
[youpdated/crypto.py](https://github.com/Void1-1/youpdated/blob/main/youpdated/crypto.py) stands alone.
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
from youpdated import crypto
|
|
382
|
+
|
|
383
|
+
blob = crypto.encrypt(b"anything", "passphrase")
|
|
384
|
+
crypto.write_private("secret.bin", blob) # atomic, 0600
|
|
385
|
+
assert crypto.decrypt(blob, "passphrase") == b"anything"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Everything in `crypto.__all__` is supported, the container format included: `encrypt`, `decrypt`, `is_encrypted`, `is_encrypted_file`, `write_private`, `prompt_passphrase`, `available`, `require_backend`, `EncryptionError`, `MAGIC`, `VERSION`, `PASSPHRASE_ENV`. A `decrypt` on a file this version can't read raises rather than guessing, and `VERSION` is bumped if the layout ever changes.
|
|
389
|
+
|
|
390
|
+
The same passphrase threads through the two core entry points, so you can drive an encrypted setup without the CLI:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
from youpdated.config import load_config
|
|
394
|
+
from youpdated.state import State
|
|
395
|
+
|
|
396
|
+
config = load_config("youpdated.yaml", passphrase="...")
|
|
397
|
+
with State("state.sqlite3", passphrase="...") as state:
|
|
398
|
+
print(state.seen_count())
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```sh
|
|
402
|
+
$ python -c "from youpdated.cli import main; main(['sources'])" && python -c "
|
|
403
|
+
import sys, youpdated.cli
|
|
404
|
+
print('cryptography loaded:', any(m.startswith('cryptography') for m in sys.modules))"
|
|
405
|
+
cryptography loaded: False
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Don't use encryption with untrusted sources or non-official code without verifying security yourself!
|
|
409
|
+
|
|
318
410
|
## Privacy
|
|
319
411
|
|
|
320
412
|
- **No credentials.** Every source works unauthenticated. `GITHUB_TOKEN` and `YOUTUBE_API_KEY` are read from the environment if set (only for raising rate limits) and are never written to config.
|
|
321
|
-
- **Local only.** Config and history live in your platform's config/data directories. Nothing is sent anywhere except the sources you list.
|
|
413
|
+
- **Local only.** Config and history live in your platform's config/data directories. Nothing is sent anywhere except the sources you list. They can be [encrypted at rest](#encryption-at-rest).
|
|
322
414
|
- **One HTTP path.** Every request goes through [youpdated/http.py](https://github.com/Void1-1/youpdated/blob/main/youpdated/http.py). `privacy.proxy` covers all traffic. To check:
|
|
323
415
|
|
|
324
416
|
```sh
|
|
325
417
|
youpdated check --all --no-save # with privacy.proxy: socks5://127.0.0.1:9
|
|
326
418
|
```
|
|
327
419
|
|
|
420
|
+
- **The proxy fails closed.** There is no direct fallback: if `privacy.proxy` is set and the proxy
|
|
421
|
+
is unreachable, requests error rather than going around it. Youpdated also checks the proxy is
|
|
422
|
+
up *before* the run starts and refuses with exit `1` if it isn't, so a run with Tor down can't
|
|
423
|
+
quietly report "nothing new" and record an empty baseline. `--test` reports the proxy as
|
|
424
|
+
unreachable instead of aborting, since it sends nothing either way.
|
|
425
|
+
- **Hostnames are resolved by the proxy, not by you.** SOCKS5 CONNECT sends the domain name to the
|
|
426
|
+
proxy (`ATYP 3`), so your local resolver never sees which sites you watch. A proxied request with a locally-resolved hostname would leak the whole list to your DNS server.
|
|
427
|
+
|
|
328
428
|
- **Cookies are discarded** on every request, so nothing accumulates across a run.
|
|
329
429
|
- **Requests are paced** per host with random jitter instead of arriving in a burst.
|
|
330
430
|
- **Conditional GETs** (ETag / Last-Modified) mean unchanged feeds are re-fetched cheaply, which is both faster and less fingerprintable. `--all` turns them off, since a 304 has no items.
|
|
@@ -21,11 +21,18 @@ Youpdated is a local CLI that reads public endpoints. The interesting attack sur
|
|
|
21
21
|
- **Privacy leaks.** Youpdated promises that when `privacy.proxy` is set, *every* request goes
|
|
22
22
|
through it, and that nothing is sent anywhere except the configured sources. A path that bypasses
|
|
23
23
|
the proxy, leaks the config or history off the machine, or attaches identifying data to requests
|
|
24
|
-
is a vulnerability, not just a bug.
|
|
24
|
+
is a vulnerability, not just a bug. This includes failing *open*: if the proxy is unreachable, no
|
|
25
|
+
request may fall back to a direct connection. It also includes resolving a watched hostname
|
|
26
|
+
locally instead of handing it to the proxy, which would leak the whole watch list to your DNS
|
|
27
|
+
resolver even though the requests themselves are proxied.
|
|
25
28
|
- **Credential handling.** `GITHUB_TOKEN` and `YOUTUBE_API_KEY` are read from the environment and
|
|
26
29
|
must never be written to disk, logged, or sent to any host other than the one they belong to.
|
|
27
30
|
- **`youpdated uninstall`.** It deletes files. Any way to make it remove something outside its own
|
|
28
31
|
config and data directories is in scope.
|
|
32
|
+
- **Encryption at rest.** When the config or history is encrypted, the passphrase and the decrypted
|
|
33
|
+
contents must exist only in memory. Anything that writes plaintext to disk, logs the passphrase,
|
|
34
|
+
accepts a file whose authentication tag does not verify, or lets the KDF parameters be weakened
|
|
35
|
+
is in scope. So is a way to read an encrypted file's contents without the passphrase.
|
|
29
36
|
|
|
30
37
|
## What's out of scope
|
|
31
38
|
|
|
@@ -34,5 +41,16 @@ Youpdated is a local CLI that reads public endpoints. The interesting attack sur
|
|
|
34
41
|
- Rate limiting or blocking by an upstream service (YouTube's feed does this intermittently by
|
|
35
42
|
design: see the README).
|
|
36
43
|
- Requiring a proxy or Tor to be running. Youpdated does not start or manage one.
|
|
44
|
+
- Anything reachable while Youpdated is running with an unlocked setup: the passphrase is in the
|
|
45
|
+
process's memory by necessity, and `YOUPDATED_PASSPHRASE` is readable by other processes of the
|
|
46
|
+
same user. Encryption at rest protects the files, not a live session.
|
|
47
|
+
- Plaintext left in free space by `youpdated encrypt`. It overwrites the file; recovering the old
|
|
48
|
+
blocks from the underlying device is out of scope, and not something a userspace tool can
|
|
49
|
+
reliably prevent on modern storage.
|
|
50
|
+
- File permissions on Windows. The `0600` mode requested when writing is a POSIX concept; Windows
|
|
51
|
+
ignores it and applies the ACL inherited from the containing directory. The default
|
|
52
|
+
`%LOCALAPPDATA%` location is already user-scoped, but a `--config` or `--state` path in a shared
|
|
53
|
+
directory is not. Restricting an ACL needs `pywin32` or shelling out to `icacls`, which this
|
|
54
|
+
project does not depend on. The file contents stay encrypted either way.
|
|
37
55
|
- Vulnerabilities in dependencies with no exploitable path through this code. Report those upstream;
|
|
38
56
|
Dependabot tracks version bumps here.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "youpdated"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Simple update tracker for games, apps, and packages"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -50,7 +50,8 @@ dependencies = [
|
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
[project.optional-dependencies]
|
|
53
|
-
|
|
53
|
+
encryption = ["cryptography>=42.0"]
|
|
54
|
+
dev = ["pytest>=8.0", "respx>=0.21", "cryptography>=42.0"]
|
|
54
55
|
publish = ["build>=1.2", "twine>=5.0"]
|
|
55
56
|
|
|
56
57
|
[project.urls]
|