Web & Email Evidence — Findings, Methods & Reproduction
The rest of this repository proves what the app does (static analysis of the decompiled APK, network captures). This document covers the surrounding evidence layer gathered to show two things the code alone cannot:
- How the public is funneled to the app — a Dutch municipality presents it as “the official TT Festival App” and hands out the link, while the app and its data processing are actually run by a third party (the organising stichting / Appmiral / Crowd Connected).
- That the responsible parties were formally notified — and, in writing, admitted the very US data transfer the app’s own privacy policy denies.
All of it is captured as timestamped, reproducible, checksum-verified snapshots
under ../evidence/. This page is the analyst’s index to those
snapshots: the findings, the techniques used to capture them, the prerequisites,
and how to reproduce each one. For the human-browsable gallery open
../evidence/index.html; to add evidence see
../evidence/CONTRIBUTING.md.
The evidence chain
flowchart TD
A["Gemeente Assen — official municipal website<br/>www.assen.nl/heeft-u-nog-vragen<br/>'Download de officiële TT Festival App via app.ttfestival.nl'"]
A -->|301 redirect| B["app.ttfestival.nl<br/>(Apache)"]
B -->|301 redirect| C["ttfestival2026.deeplink.appmiral.com<br/>Appmiral · AWS API Gateway / CloudFront — a third party"]
C --> D["Google Play<br/>com.appmiral.ttfestival"]
C --> E["Apple App Store<br/>id 6744364695"]
C --> F["app.appmiral.com/ttfestival2026/privacy-policy<br/>'All data stored within the EU (AWS eu-west-1)'<br/>English-only · no Dutch version"]
F -. contradicted by .-> G["This repo's finding:<br/>location streamed to AWS us-east-1 (USA)<br/>see docs/crowd-connected.md"]
H["ttfestival.nl — event website<br/>app-announcement post → app.ttfestival.nl"] --> B
style G fill:#fde2e2,stroke:#c0392b
The municipality presents the app under its own banner; the operator and the data
processing are a separate party. Each snapshot’s README.md holds the full
reasoning and verbatim quotes.
Findings
IDs: WE-# = web evidence, EM-# = email/correspondence evidence. Each finding links to the captured artifact that proves it.
| ID | Finding | Proven by | Status |
|---|---|---|---|
| WE-1 | Gemeente Assen calls it “de officiële TT Festival App” and distributes the download link on an official www.assen.nl page. |
assen.nl_.../ — screenshots/03_heeft-u-nog-vragen_fullpage.png, rendered-dom/, raw/ |
Captured |
| WE-2 | The “official app” link is operated by a third party: app.ttfestival.nl 301-redirects to ttfestival2026.deeplink.appmiral.com (Appmiral / AWS), not Gemeente Assen. |
appmiral.com_.../logs/redirect-chain_app.ttfestival.nl.txt |
Captured |
| WE-3 | The privacy policy’s “all data stored in the EU (eu-west-1)” claim is captured verbatim — directly testable against this repo’s us-east-1 finding. |
appmiral.com_.../raw/privacy-policy.html + docs/crowd-connected.md |
Captured + contradicted |
| WE-4 | The privacy policy is English-only — no Dutch version (no hreflang, no language negotiation). GDPR Art. 12(1) transparency concern for a Dutch audience. |
appmiral.com_.../logs/language-check_privacy-policy.txt + privacy-policy-analysis.md §8 |
Captured + verified |
| WE-5 | TT Festival house rules: entering the terrain = automatic consent to image/audio recording, used both for “openbare orde en veiligheid” and “promotionele doeleinden”. | ttfestival.nl_.../raw/info-huisregels.html + screenshot 03 |
Captured |
| WE-6 | The event site ttfestival.nl is the on-ramp to the tracking chain — its app-announcement post sends the public to app.ttfestival.nl. |
ttfestival.nl_.../raw/app-announcement.html (174/174 sitemap pages mirrored) |
Captured |
| EM-1 | Written admission of the US transfer. The organisation, relaying Crowd Connected, confirms location data is processed via AWS us-east-1 (Virginia, USA) — the exact point the policy denies. |
email-correspondence/03_ttfestival_privacyonderzoek/04_*.txt |
Admitted in writing |
| EM-2 | The municipality is on notice and acknowledged it. A municipal safety adviser replies “…dan moeten wij hier zeker iets mee” and undertakes to escalate. | email-correspondence/02_gemeente-assen_privacy-zorgen/03_*.txt |
Acknowledged |
| EM-3 | Controllership + access request. A formal GDPR Art. 15 access request and the Art. 4(7)/24 controllership question are put to Stichting TT Festival. | email-correspondence/03_ttfestival_privacyonderzoek/05_*.txt |
On record |
WE-1 · Municipal endorsement (assen.nl)
The decisive line is on /heeft-u-nog-vragen, captured verbatim:
“Meer weten over het TT Festival? Download de officiële TT Festival App via app.ttfestival.nl (externe link) of neem contact op met de organisatie via info@ttfestival.nl.”
On an official www.assen.nl government page this reads as a municipal
endorsement of the app, while it is hard for a reader to tell that the app is
not run by the municipality. Three municipal pages were frozen (/, /tt,
/heeft-u-nog-vragen) so the endorsement can be cited and reproduced even if the
page is later edited. Full reasoning:
assen.nl_2026-06-26_231242/README.md.
WE-2 · The redirect chain proves a third-party operator
https://app.ttfestival.nl/
└─ 301 Moved Permanently (Server: Apache)
→ https://ttfestival2026.deeplink.appmiral.com/download (AWS API Gateway / CloudFront)
→ Google Play com.appmiral.ttfestival / Apple App Store id 6744364695
The link the municipality calls “the official app” lands on Appmiral / AWS
infrastructure. (Note: the deeplink page returns 403 to a bare HEAD — AWS
API Gateway MissingAuthenticationTokenException — and serves real HTML only to a
browser-like GET; see the technique below.)
WE-3 / WE-4 · The captured privacy policy
The policy is the primary source for two comparisons made elsewhere in the
repo: its EU-only storage claim (§4/§7) tested against the us-east-1 streaming
finding (crowd-connected.md), and its English-only
delivery as a GDPR Art. 12 issue (privacy-policy-analysis.md §8).
Both are frozen in raw/privacy-policy.html + rendered-dom/ + a screenshot, and
the language check is a re-runnable log.
WE-5 / WE-6 · The event website (ttfestival.nl)
info/huisregels/ states verbatim that recordings “kunnen worden gebruikt in het
kader van handhaving van de openbare orde en veiligheid, maar ook voor
promotionele doeleinden” and that “door het terrein te betreden geef je
toestemming”. The whole site (174/174 sitemap pages) was mirrored so quotes
can’t be dismissed as cherry-picked; coverage is proven in
meta/coverage-report.txt.
EM-1 / EM-2 / EM-3 · The correspondence
The single most important item: the operator concedes in writing the US
(us-east-1) processing the privacy policy denies, and a municipal adviser
acknowledges the concern. The full timeline, the three threads, and the “why this
is evidence” framing are in
email-correspondence/README.md.
Every message is preserved byte-exact as the original .eml.
Prerequisites
The captures were produced on Microsoft Windows 11 Pro with the following
tools. Exact versions are recorded in each snapshot’s logs/tool-versions.txt.
| Tool | Version used | Purpose | Install |
|---|---|---|---|
| curl | 8.20.0 (Schannel) | Exact raw HTTP responses + headers; redirect chains; emoji-slug page recovery | Bundled with Windows 10/11 (curl.exe) |
| GNU Wget | 1.21.4 (mingw32) | Browsable offline full-site mirror with assets | winget install JernejSimoncic.Wget |
| Google Chrome | 149.x (driven via Chrome DevTools) | JavaScript-rendered DOM + full-page/viewport screenshots | https://www.google.com/chrome/ |
| PowerShell | 5.1 / 7.x | Orchestration, Get-FileHash integrity manifests |
Built into Windows |
| Python | 3.11.9 | Parse/extract .eml email threads (email + BytesParser) |
https://www.python.org/ |
| Node.js | v24.12.0 | Deduped download of Next.js-proxied images (assen.nl assets-extra/) |
https://nodejs.org/ |
| git | 2.53.0 | Version control; byte preservation via .gitattributes |
https://git-scm.com/ |
| gh (GitHub CLI) | current | Create the private companion repo, push | https://cli.github.com/ |
A single consistent desktop User-Agent is used across curl, wget and Chrome so every artifact reflects the same rendering path:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
Techniques used
T1 · Exact raw capture (curl)
The verbatim, unmodified server response and its response headers — the forensic ground truth:
$UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
curl.exe -s -A $UA -D raw/home.headers.txt -o raw/home.html "https://www.assen.nl/"
The saved bytes match the Content-Length in the headers, so the capture is
self-verifying.
T2 · Rendered DOM + screenshots (Chrome DevTools)
Modern sites hydrate with JavaScript, so the raw HTML isn’t what a visitor sees.
Chrome was driven at a 1440×900 desktop viewport to (a) dump the DOM after
hydration and (b) save a full-page PNG and a viewport PNG. The DOM dump is
'<!DOCTYPE html>\n' + document.documentElement.outerHTML written to
rendered-dom/<page>.html. Every screenshot was opened and confirmed non-blank
before being trusted.
T3 · Full-site mirror (wget) seeded from the sitemap
# whole site, seeded from sitemap_index.xml -> 174 page URLs
wget -r -l inf -p -k -E -e robots=off --domains=ttfestival.nl --no-check-certificate `
-U $UA -i meta/sitemap-urls.txt "https://ttfestival.nl/" `
-P mirror -o wget-mirror.log
Completeness is then cross-checked against the sitemap (URL → local file,
URL-decoding %xx) and written to meta/coverage-report.txt. Wget gotchas worth
knowing: the server sends no Last-Modified (so -N can’t resume; delete
mirror/ before a fresh run to avoid .1/.2 duplicates), and link-conversion
(-k) only runs after all downloads finish.
T4 · Emoji / multi-byte slug recovery (curl)
GNU Wget on Windows cannot write filenames containing emoji or other
multi-byte characters (e.g. the slug “Nieuwe TT Festival app 📱”). Eight live
(HTTP 200) sitemap pages were skipped by wget for this reason and recovered
directly with curl into mirror/.../_recovered/ under short, stable filenames,
with a filename → original-URL map. This is how coverage reached 174/174.
T5 · Email .eml parsing & extraction (Python)
The 10 messages were parsed with Python’s email library
(BytesParser(policy=policy.default)), sorted by the Date header, then for each
message: the raw .eml is copied byte-exact to raw/, a readable NN_*.txt
rendering is written (From/To/Cc/Date/Subject/Message-ID/In-Reply-To/Attachments +
plain-text body, falling back to stripped HTML), and attachments are saved to
NN_attachments/. The imported .eml files were verified byte-identical to
the originals via SHA-256.
T6 · Integrity manifests (SHA-256)
Every evidence folder ships a SHA256SUMS.txt (SHA-256 of every file except the
folder’s own README.md, SHA256SUMS.txt, mirror-manifest.sha256). Generate
with the repo helper, which writes LF/UTF-8/forward-slash output so verification
is identical on every OS:
pwsh tools/gen-sha256sums.ps1 -Path evidence/<host>_<timestamp>
T7 · Two-repo split + mirror cross-attestation
The heavy browsable wget mirrors (the ttfestival.nl clone alone is ~1.16 GB)
would permanently bloat the public history and re-host large third-party sites, so
they live in the private companion repo
GraafG/tt-festival-app-evidence.
Each public snapshot keeps a mirror-manifest.sha256 listing the SHA-256 of every
moved file; the private snapshot ships an identical SHA256SUMS.txt. The two lists
are byte-for-byte equal, so the public repo independently attests to exactly what
the private mirror contains.
T8 · Byte preservation in git
evidence/.gitattributes sets * -text -diff, so
git stores and checks out the exact bytes (no line-ending/encoding
normalization) on every platform — which is what keeps the SHA-256 manifests valid
after cloning.
Reproduce — quickest paths
- See what we saw: open any PNG under a snapshot’s
screenshots/, or the whole gallery via../evidence/index.html. - Re-fetch a page exactly: use the T1 curl command with the shared UA; the
saved bytes should match the captured
raw/<page>.html(and theContent-Lengthheader). The appmiral deeplink page must be fetched with a browser-likeGET(a bareHEADreturns 403). - Re-check the English-only finding (WE-4): run the two curl requests in
privacy-policy-analysis.md§8 — a Dutch-preferred request returns the same English document with zerohreflang/alternatelinks. - Re-verify the redirect chain (WE-2):
curl.exe -sIL -A $UA "https://app.ttfestival.nl/"and read theLocation:hops. - Re-mirror the whole site (T3): the exact wget command is in the private
repo’s
ttfestival.nl_2026-06-26_234242/wget-command.txt, seeded frommeta/sitemap-urls.txt(preserved in the public snapshot). -
Verify integrity of any folder:
sha256sum -c SHA256SUMS.txt # run inside the folderGet-Content SHA256SUMS.txt | ForEach-Object { $h,$p = $_ -split ' ',2 if ((Get-FileHash -Algorithm SHA256 -LiteralPath $p).Hash.ToLower() -ne $h) { "MISMATCH: $p" } }
Chain of custody / caveats
- Captures freeze public pages at a fixed point in time; URLs are preserved
unmodified. Folder names encode the local capture time
(
<host>_<YYYY-MM-DD_HHmmss>); re-capturing creates a new folder rather than overwriting. - The email
.emlfiles are the primary record and were confirmed byte-identical to source; the.txtrenderings are conveniences (if a rendering and the.emldisagree, the.emlgoverns). - The web findings here document endorsement, routing and disclosure; the
claims they are tested against (US transfer, account-linked location) are proven
in the app-analysis docs (
crowd-connected.md,identity-correlation-proof.md).
Artifact index
| Snapshot | What it captures | README |
|---|---|---|
evidence/assen.nl_2026-06-26_231242/ |
3 municipal pages (WE-1) | README |
evidence/appmiral.com_2026-06-26_232241/ |
deeplink + privacy policy (WE-2/3/4) | README |
evidence/ttfestival.nl_2026-06-26_234242/ |
full event site, 174/174 pages (WE-5/6) | README |
evidence/email-correspondence/ |
10 emails, 3 threads (EM-1/2/3) | README |
See also: ../evidence/README.md (top-level index),
../evidence/index.html (human gallery),
../evidence/CONTRIBUTING.md (how to add evidence).