# Evidence capture — Appmiral app cohort: APK harvest + integration & live-config audit (2026-06-28)

Forensic snapshot of **every Appmiral / Appstrakt app installed on the
investigator's phone**, pulled byte-for-byte from the device as APKs, then
statically scanned for the integrations Appmiral advertises on
<https://appmiral.com/features/integrations>, and — for every app carrying the
Crowd Connected location SDK — read for its **live, server-pushed config**: the
active geofence **km range** and whether the remote **kill-switch** is ON or OFF.

This **extends and supersedes** the earlier static 114-app inventory and the
hand-picked "12-app CrowdConnected club": the cohort is re-enumerated from the
live device (**140 apps now installed**), complete split-APK sets are pulled, and
the tracking analysis is driven by the **actual integration scan** rather than a
preset list.

> The APK **bytes** (375 files, ~7.08 GB) are **not** committed to this public
> repo — they live in the companion **private** repo
> [`GraafG/tt-festival-app-evidence`](https://github.com/GraafG/tt-festival-app-evidence)
> under `apks_2026-06-28_154246/`. Their byte-for-byte integrity is attested here
> by [`apk-manifest.sha256`](apk-manifest.sha256), which is **identical** to the
> private folder's `SHA256SUMS.txt`. This mirrors the repo's existing two-repo
> model (see [`../CONTRIBUTING.md`](../CONTRIBUTING.md)).

## Snapshot summary

| Field | Value |
|-------|-------|
| Captured (local) | **2026-06-28 15:42–15:59 CEST (UTC+02:00)** |
| Capture method | `adb pull` of exact on-device install bytes (no root needed for APKs) |
| Device | **Pixel 10a, Android 16, Magisk root**, build `…/stallion:16/CP1A.260505.005/15081906` |
| ADB serial | `5B161JEA308016` |
| Apps harvested | **140** (`appmiral.*`, `be.appstrakt.*`, `pl.mysticfestival`, `com.triple.verstappen`) |
| APK files | **375** split-APK files (base + `split_config.*`) |
| Total APK bytes | **~7.08 GB** (private repo) |
| Crowd Connected DBs read (root) | **6** apps with a live, populated config |

Tooling, exact versions, and the full reproduction walk-through:
[`../../docs/appmiral-comparison/10-apk-harvest-reproduction.md`](../../docs/appmiral-comparison/10-apk-harvest-reproduction.md).

## What's in this folder (public)

```
appmiral-apks_2026-06-28_154246/
├── README.md                 ← this capture log
├── SHA256SUMS.txt            ← SHA-256 of every file in THIS (public) folder
├── apk-manifest.sha256       ← SHA-256 of every harvested APK (== private SHA256SUMS.txt)
├── .capture-timestamp
├── data/                     ← machine-readable derived tables (frozen copies)
│   ├── installed_apps_2026-06-28.csv          ← 140 installed vs the old 114 inventory (+26 new)
│   ├── apk_files_manifest_2026-06-28.csv      ← 375 rows: pkg, versionName/Code, file, size, sha256, on-device path
│   ├── integration_matrix_2026-06-28.csv      ← app × 51 integration/SDK presence matrix
│   ├── integration_matrix_2026-06-28.identify.csv  ← per-app id: label, versions, minSdk/targetSdk, ABIs, location perms
│   ├── integration_matrix_2026-06-28.json     ← matrix + the exact matched byte-patterns (audit trail)
│   ├── cc_live_config_2026-06-28.csv          ← 6 apps: kill-switch, km radius, centre, cadences (identifiers redacted)
│   └── cc_live_config_2026-06-28.json
└── logs/
    ├── tool-versions.txt                       ← OS, adb, aapt2, Python, androguard, sqlite (captured programmatically)
    ├── evidence_header_2026-06-28.txt          ← device/build/wall-clock header
    └── appmiral_packages_2026-06-28.txt        ← the raw 140-package list as enumerated on device
```

The APK bytes (`apks_2026-06-28_154246/<pkg>/*.apk`) are in the **private** repo.
The Crowd Connected Room databases (which contain device/account identifiers) are
**also private** (`cc_dbs_2026-06-28_154246/`); only the **redacted** decoded
config is published here in `data/cc_live_config_*`.

## Headline findings

### 1. Advertised vs. actually-embedded integrations

Static scan of all 140 apps against Appmiral's published integration catalogue
(byte-scan of DEX + resources + native libs + manifest; presence-only, with the
matched pattern recorded for every hit in the `.json`). Counts out of 140:

| Integration | Apps /140 | Note |
|---|---:|---|
| Instagram, Firebase (Analytics/Messaging/Crashlytics), Google Fused Location | 140 | platform baseline — in **every** app |
| YouTube, AWS Cognito, CM.com, Beatswitch, **AWS Kinesis Firehose**, Google Maps SDK | 139 | near-universal |
| Ticketmaster | 137 | |
| Spotify | 128 | |
| Get Moments | 114 | |
| Facebook SDK | 52 | |
| OneTrust (consent) | 40 | |
| **Permutive** (CDP) | 36 | variable adoption |
| **Mapbox** | 16 | variable adoption |
| Billetten | 10 | |
| **Crowd Connected** (literal pkg/host) | 9 | obfuscated away in most — see below |
| Tealium | 2 | |
| **TikTok, Azure, Sports Alliance, ShowPass, Eventim, Ticketpay, Tickoweb, Bleep, SeeTickets, WeezEvents, GET, Mobile Locker, Insider, Fanalist, PwC, Halo by CM, Inventis, Jebbit, Komo** | **0** | **advertised but not present in any installed app** |

Two structural facts this exposes:

- **Platform baseline vs. per-festival reality.** Because every Appmiral app is
  built from one shared SDK/template, many "integrations" are baked into ~all 140
  apps regardless of whether a given festival uses them — so the marketing page
  **over-states** per-event usage. The *meaningful* signal is the
  **variable-adoption** integrations (Permutive 36, Mapbox 16, background-location
  30, Crowd Connected literal 9, Tealium 2) plus the **runtime config**.
- **The location-tracking capability is bundled almost everywhere.** Crowd
  Connected's own package string survives in only 9 apps (R8/obfuscation strips it
  elsewhere), but its AWS pipeline — **Cognito 139 + Kinesis Firehose 139 + Fused
  Location 140** — is near-universal. The capability ships in essentially the whole
  fleet; only the **runtime DB** proves it is *armed* in a given app.

Location permissions: **all 140** request fine + coarse location; **30** also
request **background** location.

### 2. Live config — km range + kill-switch (the 6 apps with a populated CC DB)

Read with root from each app's `CrowdConnected-RoomDatabase` (new SDK) or
`shared_prefs/CrowdConnected.xml` (legacy heartbeat). Identifiers redacted.

| App | Kill-switch | Active km range | Geofence centre | Schema |
|---|---|---:|---|---|
| **`com.appmiral.ttfestival`** | **🔴 ON** | **60 km** | 52.99, 6.56 (Assen) | new `Settings.active=1`, `Mode=BACKGROUND`, SDK 2.1.0 |
| `com.appmiral.mdlbeast` | OFF | 60 km (armed) | 21.49, 39.19 (Jeddah) | new `Settings.active=0` |
| `com.appmiral.rockforpeople` | OFF | 40 km (armed) | 50.25, 15.84 (CZ) | new `Settings.active=0` |
| `com.appmiral.teamnl` | OFF | 50 km (armed) | 45.45, 9.16 (Milan) | new `Settings.active=0` |
| `com.appmiral.museumnachtamsterdam` | OFF | 80 km (armed) | 52.36, 4.89 (Amsterdam) | legacy `activeAPIResponse=120,false,…` |
| `com.appmiral.dutchgp` | OFF | — (no zone cached this run) | — | legacy `activeAPIResponse=120,false,…` |

**Only TT Festival was live** (`active=1`) at capture time, with a **60 km**
collection ring centred on Assen — far wider than the festival grounds — and the
SDK in **background** mode. The kill-switch is **server-pushed**: every other app
carried an armed geofence (40–80 km) but was toggled **OFF** by Crowd Connected's
backend, demonstrating the on/off is a remote decision, not a local one. Config is
fetched by the SDK from its backend during normal polling; we only read the cached
result locally and never contacted Crowd Connected.

> **Determinism caveat:** server-pushed config drifts over time, and only apps
> that have been *launched* have a populated DB — so a replicator re-running this
> later may see different switch states / different apps with cached config. That
> is expected and itself documents that the operator controls the switch remotely.

Full analysis: [`08-integration-matrix`](../../docs/appmiral-comparison/08-integration-matrix.md)
and [`09-active-config-km-killswitch`](../../docs/appmiral-comparison/09-active-config-km-killswitch.md).

## Verify integrity

This (public) folder:

```bash
sha256sum -c SHA256SUMS.txt          # Linux/macOS, run inside this folder
```

```powershell
Get-Content SHA256SUMS.txt | ForEach-Object { $h,$p = $_ -split '  ',2
  if ((Get-FileHash -Algorithm SHA256 -LiteralPath $p).Hash.ToLower() -ne $h) { "MISMATCH: $p" } }
```

The APK bytes in the private repo, against the public manifest:

```bash
# inside the private repo's apks_2026-06-28_154246/ folder
sha256sum -c /path/to/this/folder/apk-manifest.sha256
```

`apk-manifest.sha256` here is byte-for-byte identical to
`apks_2026-06-28_154246/SHA256SUMS.txt` in the private repo, so this public record
independently attests exactly which APK bytes were captured.

## Reproduce

Everything was produced by the documented, parameterized scripts in
[`../../tools/apk-harvest/`](../../tools/apk-harvest/) (no hard-coded serial; idempotent).
Follow the step-by-step guide
[`10-apk-harvest-reproduction.md`](../../docs/appmiral-comparison/10-apk-harvest-reproduction.md)
— prerequisites, exact commands, expected outputs, checksum verification, and the
determinism caveats above.
