Skip to content

Add reproducible Exodus tracker-signature updater script (#318) - #622

Merged
kasnder merged 1 commit into
masterfrom
claude/refresh-exodus-signatures-318
Jul 11, 2026
Merged

Add reproducible Exodus tracker-signature updater script (#318)#622
kasnder merged 1 commit into
masterfrom
claude/refresh-exodus-signatures-318

Conversation

@kasnder

@kasnder kasnder commented Jul 11, 2026

Copy link
Copy Markdown
Member

Addresses #318.

What changed

  • scripts/update_exodus_trackers.py — a robust, idempotent updater that refreshes the bundled Exodus tracker signatures from the official Exodus JSON API, replacing the fragile HTML scraper proposed in the issue.
  • README.md — one acknowledgement line documenting the Exodus data source and the refresh script.

No tracker data is bundled in this PR (see verification below).

Data flow I mapped

  • TrackerControl detects tracker libraries in app code and maps traffic to trackers using Exodus Privacy signatures.
  • TrackerSignatureManager (app/src/main/java/.../analysis/) already downloads the latest signatures at runtime from https://reports.exodus-privacy.eu.org/api/trackers and caches them in filesDir/trackers.json.
  • The committed asset app/src/main/assets/trackers.json is the offline fallback loaded on first launch / when the live fetch fails. Its schema is the API response verbatim: {"trackers": {"<id>": {id, name, code_signature, network_signature, website, categories, ...}}}.
  • The parser (parseTrackers) reads id, name, code_signature, network_signature, website. Current asset: 432 trackers, 260 with network signatures, 428 with code signatures (newest creation_date 2023-12-05).

The issue's HTML scraper of reports.exodus-privacy.eu.org is unnecessary and non-reproducible — Exodus exposes a documented JSON API, which is exactly the source the app already consumes.

How a maintainer refreshes the bundled fallback

python3 scripts/update_exodus_trackers.py           # fetch, validate, rewrite asset if changed
python3 scripts/update_exodus_trackers.py --check    # validate only, never write
python3 scripts/update_exodus_trackers.py --input f  # validate a local dump

Stdlib-only (no dependencies). The script validates before writing (top-level trackers object, required id/name, sane count, non-empty network signatures, regex compile sanity check), writes the raw API bytes verbatim so the committed asset stays byte-identical to what the app downloads, and only rewrites on an actual change.

Verification status

  • Live run today: fetched the API and the updater reported Asset already up to date — the live API is byte-identical to the committed asset, so a data refresh right now is a verified no-op. This is why no data blob is committed here.
  • Idempotency / positive path: exits 0 with correct stats (432 / 260 / 428).
  • Negative path: truncated payload and missing-trackers payload both correctly fail with exit 1 and no write.
  • No Java touched (no compile needed); no native/Rust changes.

Licensing

The tracker signatures are produced by Exodus Privacy and distributed under their own terms. This PR commits no Exodus data — the script only mirrors their published API output on demand, and the licensing note is in the script header and README.

🤖 Generated with Claude Code

TrackerControl detects tracker libraries and maps traffic using signatures
from Exodus Privacy. The app already fetches the latest signatures from the
official Exodus JSON API at runtime (TrackerSignatureManager); the copy at
app/src/main/assets/trackers.json is the offline fallback.

Issue #318 asked for a way to refresh/expand these signatures and proposed a
fragile HTML scraper of reports.exodus-privacy.eu.org. This replaces that with
scripts/update_exodus_trackers.py, which pulls from the same documented JSON
API the app uses, validates the payload against the schema the app's parser
expects (top-level "trackers" object, required id/name fields, sane tracker
count, non-empty network signatures, regex compile check), and rewrites the
bundled asset only when content actually changed. --check validates without
writing; --input reads a local file.

No data change is bundled: the live API is currently byte-identical to the
committed asset (432 trackers, newest 2023-12-05), so refreshing today is a
verified no-op. The script gives maintainers a one-command, idempotent refresh
for whenever Exodus updates upstream.

Exodus signature data remains under Exodus Privacy's own terms; the script
only mirrors their published API output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kasnder
kasnder marked this pull request as ready for review July 11, 2026 12:50
@kasnder
kasnder merged commit 8e458f4 into master Jul 11, 2026
1 check passed
kasnder added a commit that referenced this pull request Jul 11, 2026
…ft flag

The app bundles several remote datasets in app/src/main/assets/ that are read
straight from the read-only APK, so they can only be refreshed by shipping a new
build. Only the on-device hosts blocklist had an update path; the identification
lists (Disconnect/shavar, DuckDuckGo TDS, GeoLite2) were refreshed by hand, which
is easy to forget and left their provenance undocumented.

Rather than add on-device fetches (a privacy cost for data that changes slowly,
and GeoLite2 is license-gated anyway), refresh them at build time:

- scripts/update_disconnect_list.py — Disconnect services.json -> byte-reverse
- scripts/update_ddg_tds.py — DuckDuckGo Android TDS -> field-strip to the
  fields the loader reads ({owner:{name,displayName}, default})
- scripts/update_geolite2.py — MaxMind GeoLite2 (needs MAXMIND_LICENSE_KEY)

Each is stdlib-only, validates before writing, writes only on change, and has
--check/--input, matching the convention of scripts/update_exodus_trackers.py
(#622), which stays the single Exodus updater.

.github/workflows/update-lists.yml runs these monthly and opens a PR; the PR's
existing Test CI is the gate that vets a bad upstream.

Disconnect category drift:
- TrackerControl already handles unknown Disconnect categories safely (they fold
  to Uncategorised via canonicalise, #571) — but silently. The Disconnect->bucket
  mapping is refactored out of an inline switch in loadDisconnectTrackers into
  TrackerCategory (DISCONNECT_ALIASES + mapDisconnectCategory /
  isRecognisedDisconnectCategory) so it is one testable source of truth.
- DisconnectCategoryCoverageTest reads the bundled list and fails the build if a
  category can no longer be mapped to a UI bucket, turning silent drift into a
  loud CI failure regardless of how the asset was updated.

No asset data is changed in this PR; the updaters were run and reverted so this
ships mechanism only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kasnder added a commit that referenced this pull request Jul 11, 2026
…ft flag

The app bundles several remote datasets in app/src/main/assets/ that are read
straight from the read-only APK, so they can only be refreshed by shipping a new
build. Only the on-device hosts blocklist had an update path; the identification
lists (Disconnect/shavar, DuckDuckGo TDS, GeoLite2) were refreshed by hand, which
is easy to forget and left their provenance undocumented.

Rather than add on-device fetches (a privacy cost for data that changes slowly,
and GeoLite2 is license-gated anyway), refresh them at build time:

- scripts/update_disconnect_list.py — Disconnect services.json -> byte-reverse
- scripts/update_ddg_tds.py — DuckDuckGo Android TDS -> field-strip to the
  fields the loader reads ({owner:{name,displayName}, default})
- scripts/update_geolite2.py — MaxMind GeoLite2 (needs MAXMIND_LICENSE_KEY)

Each is stdlib-only, validates before writing, writes only on change, and has
--check/--input, matching the convention of scripts/update_exodus_trackers.py
(#622), which stays the single Exodus updater.

.github/workflows/update-lists.yml runs these monthly and opens a PR; the PR's
existing Test CI is the gate that vets a bad upstream.

Disconnect category drift:
- TrackerControl already handles unknown Disconnect categories safely (they fold
  to Uncategorised via canonicalise, #571) — but silently. The Disconnect->bucket
  mapping is refactored out of an inline switch in loadDisconnectTrackers into
  TrackerCategory (DISCONNECT_ALIASES + mapDisconnectCategory /
  isRecognisedDisconnectCategory) so it is one testable source of truth.
- DisconnectCategoryCoverageTest reads the bundled list and fails the build if a
  category can no longer be mapped to a UI bucket, turning silent drift into a
  loud CI failure regardless of how the asset was updated.

No asset data is changed in this PR; the updaters were run and reverted so this
ships mechanism only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant