Metadata-Version: 2.5
Name: apkradar
Version: 2026.44
Summary: APK compliance auditor — tracker detection, permissions analysis, GDPR art.9
Author-email: Massimo Moretti <security@maksimtech.com>
License: MIT License
        
        Copyright (c) 2026 maksimtech
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: android,apk,compliance,gdpr,privacy,tracker
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Requires-Dist: androguard>=4.0.0
Requires-Dist: cookieradar>=2026.43
Requires-Dist: google-play-scraper>=1.2.7
Requires-Dist: httpx>=0.24.0
Requires-Dist: jinja2>=3.1.0
Requires-Dist: mailradar>=2026.43
Requires-Dist: openpyxl>=3.1.0
Requires-Dist: rich>=13.0.0
Requires-Dist: typer>=0.9.0
Provides-Extra: dev
Requires-Dist: hypothesis>=6.140; extra == 'dev'
Requires-Dist: mutmut>=3.8; extra == 'dev'
Requires-Dist: mypy>=1.18; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest-timeout>=2.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: pyyaml>=6.0; extra == 'dev'
Requires-Dist: ruff>=0.16; extra == 'dev'
Description-Content-Type: text/markdown

# APKRadar

APKRadar is a command-line tool that audits Android apps (`.apk`, `.xapk`,
`.apkm`) for GDPR-relevant findings: third-party tracking SDKs, sensitive
permissions and SDK vendors located outside the EU. It gives each app a
score from 0 to 100. It can also check the publisher's email security, SSL
certificate and cookie consent behaviour, and prepare a formal GDPR letter to
the publisher's Data Protection Officer (DPO).

```
pip install apkradar
apkradar audit app.apk
```

## Contents

- [Why it matters for the GDPR](#why-it-matters-for-the-gdpr)
- [What APKRadar is and is not](#what-apkradar-is-and-is-not)
- [Installation](#installation)
- [Commands](#commands)
- [Usage](#usage)
- [Reading the results](#reading-the-results)
- [Supported formats](#supported-formats)
- [Full stack analysis with MailRadar and CookieRadar](#full-stack-analysis-with-mailradar-and-cookieradar)
- [Known limitations](#known-limitations)
- [Development](#development)
- [License](#license)

## Why it matters for the GDPR

A mobile app is often the main way a company collects personal data, and much
of that collection is done by code the company did not write: analytics,
advertising and crash-reporting SDKs bundled into the app. Many of these send
data to their vendors, often outside the EU. APKRadar makes those components
visible by reading the app file itself, without running the app and without
relying on what the privacy policy says.

Each type of finding relates to a specific GDPR obligation. The same mapping is
used in the DPO letter generated by `apkradar send`:

| Finding | What APKRadar reports | GDPR provisions |
|---|---|---|
| Tracking SDKs | Known analytics, advertising and crash-reporting SDKs found in the app | Art. 5(1)(a) lawfulness and transparency, art. 13 information to the data subject |
| Sensitive permissions | Permissions that give access to location, contacts, camera, microphone, SMS, call log, biometrics and similar data | Art. 5(1)(c) data minimisation, art. 6 lawful basis |
| Permissions reaching a special category | Only the few that can: body sensors, physical activity, Health Connect records. Location is not one, and an on-device biometric unlock is not one either — the app receives a boolean, not a template | Art. 9 special categories, asked as a question because the article turns on the purpose and a manifest states none |
| Extra-EU transfers | Code from vendors headquartered outside the EU (for example Google, Meta, ByteDance, Yandex) | Art. 44–49 transfers to third countries |
| Distribution marking (`send --tlp`) | A FIRST TLP 2.0 label on the letter: in the subject line and in a block at the top of the body, saying what the recipient may do with it. Omitted, the letter is unmarked — which is not the same as TLP:CLEAR | FIRST TLP 2.0 standard, cited with its version |
| Technical measures (`--full`, `send`) | Publisher email security score, SSL certificate status, trackers active before or after cookie consent | Art. 32 security of processing |

## What APKRadar is and is not

APKRadar performs **static analysis**: it inspects the contents of the app file.
Its results are **technical indicators**. They are not proof that personal data
is actually processed or transferred.

- A detected SDK shows that the code is present in the app. It does not show
  whether the SDK is active, what data it sends, or whether consent was
  collected first.
- A sensitive permission shows that the app declares it may ask for that
  access. It does not show that the user granted it or that the data is used.
- An extra-EU transfer shows that the app contains code published by a
  non-EU company. It does not show that data leaves the EU, or which
  safeguards (such as Standard Contractual Clauses) are in place.

The findings are a good basis for questions to a controller, which is how
`apkradar send` uses them. They do not replace legal assessment or dynamic
testing (observing the app's network traffic).

## Installation

APKRadar requires Python 3.11 or later.

```
pip install apkradar
playwright install chromium
```

`pip install apkradar` installs everything needed for `audit`, `batch`,
`batch-excel`, `search` and `send`, including
[MailRadar](https://pypi.org/project/mailradar/) and
[CookieRadar](https://pypi.org/project/cookieradar/).

`playwright install chromium` downloads the headless browser that CookieRadar
uses. It is only needed for the cookie check in `audit --full`. On a fresh Linux
system Chromium may also need system libraries; install them with:

```
playwright install --with-deps chromium
```

Check the installation:

```
$ apkradar --version
APKRadar 2026.44
```

## Commands

| Command | Purpose |
|---|---|
| `apkradar audit <file>` | Audit one app file |
| `apkradar audit <file> --full` | Audit the app, then check the publisher and SDK domains with MailRadar, SSL and CookieRadar (`--offline` skips the Google Play lookup) |
| `apkradar batch <list.txt>` | Audit every app file listed in a text file |
| `apkradar batch-excel <file.xlsx>` | Audit the apps listed in an Excel sheet and write the results to Excel |
| `apkradar search <package>` | Look up an app on Google Play by package name |
| `apkradar send <file> ...` | Audit an app and send a GDPR letter to the publisher's DPO |
| `apkradar --version` | Print the installed version |

Run `apkradar <command> --help` for the full list of options.

## Usage

The examples below are real output from APKRadar 2026.44, with public
builds of Bitwarden (GitHub release `v2026.9.0-bwpm`, SHA-256
`954967bb3c9b940c16edb981242461b03c34f0e60671f42793c07a46190d64b3`),
Wikipedia and Nextcloud (F-Droid). Colours are not shown.

The `audit` example is generated from the renderer by
`tests/test_readme_example.py`, which fails if this document stops matching what
the tool prints. That check has no APK, so it cannot notice the tracker database
improving on the real file — which is exactly what made the previous example
stale — hence the hash above: re-run the tool on that artifact to confirm the
findings. Results for third-party apps and domains change over time.

### Audit an app

```
$ apkradar audit bitwarden.apk

Auditing bitwarden.apk...

📱 APKRadar Report — com.x8bit.bitwarden
Score: 70/100 — MODERATE
100 − 10 (1 tracker) − 15 (3 sensitive permissions) − 5 (1 transfer) = 70

App:     Bitwarden
Version: 2026.9.0 (21909)
SDK:     min=29 target=37
Format:  APK
SHA256:  954967bb3c9b940c16edb981242461b03c34f0e60671f42793c07a46190d64b3

                 🔴 Trackers (1)
╭─────────────┬─────────────────────────────────╮
│ Tracker     │ Package                         │
├─────────────┼─────────────────────────────────┤
│ Crashlytics │ com.google.firebase.crashlytics │
╰─────────────┴─────────────────────────────────╯
  ⚠️  Sensitive Permissions (3)
╭─────────────────┬──────────────╮
│ Permission      │ GDPR Concern │
├─────────────────┼──────────────┤
│ USE_BIOMETRIC   │ biometrics   │
│ USE_FINGERPRINT │ fingerprint  │
│ CAMERA          │ camera       │
╰─────────────────┴──────────────╯
      🌍 Extra-EU Transfers (1)
╭──────────────────┬────────────────╮
│ Entity           │ Package Prefix │
├──────────────────┼────────────────┤
│ Google LLC (USA) │ com.google     │
╰──────────────────┴────────────────╯

⚖️  Provisions applied
GDPR: verified against EUR-Lex (CELEX 32016R0679)
Contenuti digitali dir. 2019/770: verified against EUR-Lex (CELEX 32019L0770)
Codice del Consumo D.Lgs. 206/2005: verified against Normattiva (URN
urn:nir:stato:decreto.legislativo:2005-09-06;206)
  testo vigente
...
```

Score: 100 − 1 tracker × 10 − 3 permissions × 5 − 1 transfer × 5 = 70. The
Google LLC entry covers both Crashlytics and the Google components that are not
on the tracker list: Firebase Cloud Messaging (push notifications), ML Kit and
Play Core. For more on this, see
[Reading the results](#reading-the-results).

The `⚖️  Provisions applied` block lists, for each finding, the provision it
relates to and the hash of the legal text APKRadar verified it against — so a
citation can be checked against the source, not taken on trust. It is elided
above because those hashes and dates change whenever the cached legal texts are
refreshed.

The command exits with code 1 if the file cannot be analysed.

### Full stack analysis

```
$ apkradar audit wikipedia.apk --full --offline
...
✅ No extra-EU transfers detected


🔗 Full stack analysis — 1 domains

🔗 wikipedia.org (guessed from package name — unverified)
  📡 MailRadar: 49/100 — POOR
  🔒 SSL: valid until 03/11/2026
  🍪 CookieRadar: 0 pre-consent trackers, none persist
```

Add `--verbose` (`-v`) to print each step as it runs. See
[Full stack analysis](#full-stack-analysis-with-mailradar-and-cookieradar) for
what each line means.

### Audit several apps

Create a text file with one app file path per line. Empty lines and lines
starting with `#` are ignored.

```
# apks.txt
wikipedia.apk
bitwarden.apk
nextcloud.apk
```

```
$ apkradar batch apks.txt

Loaded 3 APKs from apks.txt

Auditing wikipedia.apk...
  🟡 MODERATE — 70/100 — 0 trackers, 6 sensitive permissions

Auditing bitwarden.apk...
  🟢 GOOD — 80/100 — 0 trackers, 3 sensitive permissions

Auditing nextcloud.apk...
  🟠 POOR — 55/100 — 0 trackers, 9 sensitive permissions
```

The list must contain paths to app files on disk, not URLs or package names.
If any app fails to scan, the command prints how many failed and exits with
code 1.

### Audit apps from an Excel sheet

`batch-excel` reads the first sheet of an `.xlsx` file. The first row must be a
header. Column names are case-insensitive:

| Column | Accepted names | Required |
|---|---|---|
| App file path | `APK Path`, `APK`, `Path`, `File` | Yes, for the app to be audited |
| Package name | `Package Name`, `Package`, `PackageName`, `Pkg` | No |
| App name | `App Name`, `App`, `Name`, `Title` | No |

Example input (`apps.xlsx`):

| App Name | Package Name | APK Path |
|---|---|---|
| Wikipedia | org.wikipedia | wikipedia.apk |
| Bitwarden | com.x8bit.bitwarden | bitwarden.apk |
| Coin Master | com.moonactive.coinmaster | |

```
$ apkradar batch-excel apps.xlsx --output report.xlsx

Reading apps.xlsx...
Found 3 apps to audit

Auditing Wikipedia...
  🟡 MODERATE — 70/100 — 0 trackers, 6 sensitive permissions
Auditing Bitwarden...
  🟢 GOOD — 80/100 — 0 trackers, 3 sensitive permissions
Auditing Coin Master...
  ⏭️  SKIPPED — no APK path

Writing results to report.xlsx...
✅ Report saved to report.xlsx
⏭️  1/3 skipped — no APK path
```

Without `--output`, the report is written next to the input file as
`<name>_report.xlsx` (here `apps_report.xlsx`).

APKRadar does not download apps. Rows that have a package name but no file
path are marked `SKIPPED`, with an empty score.

The output workbook contains these columns: App Name, Package Name, Format,
Version, Score, Grade (colour-coded), Trackers, Sensitive Permissions,
Extra-EU Transfers, Tracker Names and SHA256 — the whole digest, so the
artifact each row describes can be identified and checked.

With `--augment` (`-a`), the result columns (Score, Grade, Trackers,
Permissions, Extra-EU, Tracker Names) are appended to the input sheet instead.
The file is written to `--output` if given, otherwise **the input file is
overwritten**.

Every text value APKRadar writes (app names, package names, versions, tracker
names) is stored as text, never as a formula. A crafted app name or input row
therefore cannot inject a formula into the report.

### Look up an app on Google Play

```
$ apkradar search org.wikipedia

Looking up org.wikipedia...

📱 Wikipedia
Package:   org.wikipedia
Developer: Wikimedia Foundation
Category:  Books & Reference
Installs:  50,000,000+
Rating:    4.3/5.0

The best Wikipedia experience on your Mobile device. Ad-free and free of charge, forever.
...
```

If the package is not on Google Play, APKRadar searches the web (DuckDuckGo)
for a possible removal reason:

```
$ apkradar search com.example.doesnotexist.app

Looking up com.example.doesnotexist.app...

⚠️  App not found on Google Play
Package: com.example.doesnotexist.app
No removal reason found — app may have been removed or never published.
```

Only package names are supported. If you pass an app name such as
`"Wikipedia"`, APKRadar prints instructions for finding the package name on
Google Play.

### Send a GDPR letter to the DPO

`send` audits the app, checks the publisher domain with MailRadar and SSL, and
writes a formal letter to the publisher's DPO. The letter asks for the legal
basis, purposes and safeguards for each finding, and exercises the data
subject's rights of access (art. 15), information (art. 13), erasure (art. 17)
and objection (art. 21). It also states that a complaint to the supervisory
authority (art. 77) may follow if there is no reply within 30 days.

Always preview first with `--dry-run`. Nothing is sent, and no SMTP password
is asked for.

```
$ apkradar send wikipedia.apk \
    --to dpo@example.org \
    --publisher "Example Publisher" \
    --from jane@example.org \
    --name "Jane Doe" \
    --smtp-host smtp.example.org \
    --smtp-user jane \
    --noyb \
    --dry-run
...
Running MailRadar on wikipedia.org...
📡 MailRadar — wikipedia.org: 49/100 — POOR
Checking SSL on wikipedia.org...
🔒 SSL valid until 03/11/2026

--- DPO Letter Preview ---

Oggetto: Esercizio diritti GDPR — Wikipedia (org.wikipedia) — Audit tecnico indipendente

Spettabile Example Publisher,
All'attenzione del Responsabile della Protezione dei Dati
...
2. PERMESSI SENSIBILI — art. 5(1)(c), 9 GDPR

L'applicazione dichiara i seguenti permessi con rilevanza GDPR:

  → ACCESS_COARSE_LOCATION: approximate location
  → ACCESS_FINE_LOCATION: precise GPS location
  ...
```

The letter has six sections:

1. Third-party trackers and SDKs (art. 5(1)(a), 13), with the SHA-256 hash of
   the audited file
2. Sensitive permissions (art. 5(1)(c), 9)
3. Extra-EU transfers (art. 44–49), with a request to state the art. 46
   safeguards
4. Technical measures (art. 32): the MailRadar score, described as inadequate
   below 60, and the SSL certificate if it is expired, self-signed or issued by
   an unknown authority
5. Formal requests (art. 15, 13, 17, 21)
6. Escalation (art. 77, and optionally [NOYB](https://noyb.eu))

Options:

| Option | Meaning |
|---|---|
| `--to` | DPO email address (required) |
| `--publisher` | Publisher name used in the salutation (required) |
| `--from` | Sender email (required) |
| `--name` | Sender full name (required) |
| `--org` | Sender organisation |
| `--smtp-host`, `--smtp-user` | SMTP server and username (required) |
| `--smtp-port` | SMTP port. Default 465 (implicit TLS). Any other port uses STARTTLS. |
| `--publisher-domain` | The publisher's own domain, if you have established it. Without it, section 4 of the letter makes no art. 32 finding |
| `--offline` | Do not ask Google Play who publishes the app |
| `--noyb` | Add a reference to NOYB in the escalation section |
| `--noyb-id` | Add a reference to NOYB with your supporter number (implies `--noyb`) |
| `--dry-run` | Print the letter without sending it |

Without `--dry-run`, the SMTP password is asked for interactively and is never
passed on the command line. APKRadar verifies the SMTP server's certificate
before logging in. No letter is sent if the scan fails.

The letter is currently available in **Italian only**. `--lang` is accepted,
but no other template exists yet, so the Italian letter is always used.

## Reading the results

### Score

Every app starts at 100 points. Points are deducted for each finding:

| Finding | Deduction |
|---|---|
| Tracker SDK | −10 each |
| Sensitive permission | −5 each |
| Extra-EU vendor | −5 each |

The report prints the arithmetic under the score, so the number can be checked
without reading the source:

```
Score: 70/100 — MODERATE
100 − 10 (1 tracker) − 15 (3 sensitive permissions) − 5 (1 transfer) = 70
```

The printed score never goes below 0, but the sum can: an app with sixteen
tracking SDKs comes to −95, and the line then reads
`… = −95, clamped to 0`. Below zero the score stops distinguishing one app from
another, which is why the clamp is stated rather than hidden.

It maps to a grade:

| Score | Grade |
|---|---|
| 80–100 | 🟢 GOOD |
| 60–79 | 🟡 MODERATE |
| 40–59 | 🟠 POOR |
| 0–39 | 🔴 CRITICAL |

A file that cannot be analysed has **no** score: the report shows `N/A` and the
error, because 0 is the score of the worst possible app and not the absence of a
measurement. In `batch-excel`, rows without an app file are shown as `SKIPPED`
and the score cell is left empty.

The score is a way to compare and sort apps. It is not a legal assessment:
one tracker without a valid legal basis can matter more than several
permissions that are justified.

It also weighs breadth of functionality heavily. Five sensitive permissions cost
more than two tracking SDKs, so an app with no third-party code and no extra-EU
transfers can score below one that has both — Nextcloud 35.0.0 scores 55 with
nine permissions and nothing else, Bitwarden 70 with three permissions, one
tracker and one transfer to Google. The arithmetic line shows which is which, and
it is worth reading before comparing two scores.

### Trackers

APKRadar recognises 43 SDKs, including Google Analytics, Firebase Analytics,
Google Ads, Facebook App Events, Facebook Audience Network, AppsFlyer, Adjust,
Amplitude, Branch, Mixpanel, Segment, OneSignal, AppLovin, Unity Ads,
IronSource, TikTok SDK, Yandex Metrica, Crashlytics, Sentry and Huawei
Analytics. The full list is `TRACKER_SIGNATURES` in
[`apkradar/scanner.py`](apkradar/scanner.py).

An SDK is detected in two ways:

1. **Manifest**: the app declares a component (activity, service, receiver,
   provider) that belongs to the SDK's package.
2. **Code**: the SDK's classes are found in the app's compiled code (DEX
   files). This finds SDKs that declare no manifest component, such as
   Firebase Analytics, AppsFlyer and Facebook App Events.

Each SDK is listed once, with the package that identified it.

### Sensitive permissions

APKRadar flags 24 Android permissions that give access to personal data. Each
one is shown with a short description in the "GDPR Concern" column:

| Area | Permissions |
|---|---|
| Location | `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION`, `ACCESS_BACKGROUND_LOCATION` |
| Communications | `READ_CONTACTS`, `WRITE_CONTACTS`, `READ_CALL_LOG`, `WRITE_CALL_LOG`, `READ_SMS`, `RECEIVE_SMS`, `PROCESS_OUTGOING_CALLS` |
| Device identity | `READ_PHONE_STATE`, `READ_PHONE_NUMBERS`, `GET_ACCOUNTS` |
| Camera and microphone | `CAMERA`, `RECORD_AUDIO` |
| Body and biometrics | `BODY_SENSORS`, `BODY_SENSORS_BACKGROUND`, `ACTIVITY_RECOGNITION`, `USE_BIOMETRIC`, `USE_FINGERPRINT` |
| Storage and calendar | `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE`, `READ_CALENDAR`, `WRITE_CALENDAR` |

These are permissions the app **declares**. Whether a permission is justified
depends on what the app does. A password manager needs `CAMERA` to scan QR
codes, and `USE_BIOMETRIC` to unlock with a fingerprint.

### Extra-EU transfers

APKRadar lists a vendor when the app contains code from that vendor's
namespace. It knows 13 vendors:

| Package prefix | Entity as reported |
|---|---|
| `com.google` | Google LLC (USA) |
| `com.facebook` | Meta Platforms Inc. (USA) |
| `com.amazon` | Amazon Web Services (USA) |
| `com.microsoft` | Microsoft Corporation (USA) |
| `com.appsflyer` | AppsFlyer Ltd. (USA/Israel) |
| `com.adjust` | Adjust GmbH (Germany) → USA |
| `com.amplitude` | Amplitude Inc. (USA) |
| `com.mixpanel` | Mixpanel Inc. (USA) |
| `com.tiktok` | ByteDance Ltd. (China/USA) |
| `com.yandex` | Yandex LLC (Russia) |
| `com.huawei` | Huawei Technologies (China) |
| `com.xiaomi` | Xiaomi Corporation (China) |
| `com.baidu` | Baidu Inc. (China) |

A vendor is listed if the app declares a component under its prefix, or if a
tracker from that vendor was detected. This is broader than the tracker list.
Any Google component counts, including push notifications, Play Services and
ML Kit, as in the Bitwarden example above. Treat this finding as "code from
this vendor is present, so ask how transfers to it are governed". It does not
show that data is transferred.

### Hosts in the code

The check above reads *packages*, so it sees a vendor only when that vendor ships
an SDK. A service reached over plain HTTP ships nothing, and until 2026-10-02
APKRadar could not see it at all: the domain list came from the package name, the
Play listing and the manifest's deep links, none of which is where an app keeps
the servers it talks to. Those are string literals in the DEX, and R8 does not
touch them — it renames classes and methods, so a release build says nothing about
its dependencies, but the URLs survive verbatim.

Breezy Weather 6.2.2 is the case that prompted it. The manifest declared one deep
link and the package name guessed one domain; the DEX held **161** hosts, and the
audit said "no extra-EU transfers" — correctly, because AccuWeather, NOAA, JMA,
Baidu and Xiaomi all ship no SDK:

```
🌐 Hosts in the code (153 endpoints, 8 references)
Carried in the DEX — reachable by the code, not observed in traffic.
    🌍 Of those, under a vendor this tool reports on (5)
┌──────────────────────────────┬────────────────────────────┐
│ Host                         │ Entity                     │
├──────────────────────────────┼────────────────────────────┤
│ api.map.baidu.com            │ Baidu Inc. (China)         │
│ lbs.baidu.com                │ Baidu Inc. (China)         │
│ privacy.mi.com               │ Xiaomi Corporation (China) │
│ weatherapi.intl.xiaomi.com   │ Xiaomi Corporation (China) │
│ weatherapi.market.xiaomi.com │ Xiaomi Corporation (China) │
└──────────────────────────────┴────────────────────────────┘
```

Three things about this finding are deliberate.

**It changes no score.** A host in a string literal is a host the code *can*
reach, which is not traffic. An app offering fifty weather providers carries fifty
endpoints and contacts the one that is configured, so deducting a point per host
would penalise choice. The line under the count says so in the report itself.

**It is not audited by `--full`.** That would be MailRadar, an SSL check and
CookieRadar on 153 domains, about servers the app may never contact.

**References are labelled, not dropped.** `www.w3.org` and `www.opengis.net`
arrive as XML namespace URIs — identifiers that nothing dereferences — so they are
counted separately. The list of what counts as a reference is a short one of
standards bodies, licences and schemas: anything arguable stays an endpoint,
because a tool that silently reclassifies an endpoint as documentation hides
precisely what it was built to show.

Only `http://` and `https://` literals count. A bare hostname is
indistinguishable from a class name or an author's e-mail domain, and endpoints
built by concatenation (`"https://" + host`) cannot be recovered at all.

## Supported formats

| Format | Source | How it is analysed |
|---|---|---|
| `.apk` | Standard Android package | Analysed directly |
| `.xapk` | APKPure bundle | The base APK is extracted to a temporary folder and analysed |
| `.apkm` | APKMirror bundle | The base APK is extracted to a temporary folder and analysed |

The format is detected from the file extension, or from the file contents if
the extension is missing or different. For bundles, APKRadar takes the base APK
and ignores split APKs (files named `split_*` or `config.*`). See
[Known limitations](#known-limitations).

APKRadar computes the SHA-256 hash of the file as supplied. The console and
Excel reports show its first 16 characters, and the DPO letter includes it in
full. This lets you show later exactly which file was audited.

## Full stack analysis with MailRadar and CookieRadar

`apkradar audit <file> --full` also looks at the websites behind the app. It
collects these domains:

- the **publisher domain candidates**, each labelled with where it came from:
  reversing the package name (`org.wikipedia` → `wikipedia.org`; generic
  segments are skipped, so `com.game.asteroids_revenge` →
  `asteroids-revenge.com`) and the developer website on the Google Play listing.
  Both sources are guesses, and they are treated as such — see
  [Which domain belongs to the publisher](#which-domain-belongs-to-the-publisher)
- the **known domains of every detected tracker SDK**, for example
  `appsflyer.com` for AppsFlyer, or `googleadservices.com` and
  `doubleclick.net` for Google Ads
- the **deep-link hosts declared in the manifest**, which are often the
  publisher's own site. Links to a platform — the Play store page, a social
  profile — are skipped: nearly every app declares one.

For each domain it runs three checks:

| Line | Tool | What it checks |
|---|---|---|
| 📡 MailRadar | [MailRadar](https://pypi.org/project/mailradar/) | Email security of the domain, as a score from 0 to 100 with a grade. `unavailable` if the check could not run. |
| 🔒 SSL | Built in | The HTTPS certificate on port 443, using TLS 1.2 or later. Possible results: `valid until <date>`, `EXPIRED`, `self-signed certificate`, `unknown certificate authority`, `hostname mismatch`, `certificate verification failed`, `timeout`, `check failed`. |
| 🍪 CookieRadar | [CookieRadar](https://pypi.org/project/cookieradar/) | Loads the website in a headless browser and records trackers before consent and after the cookie banner is rejected. |

The CookieRadar line reads:

- `N pre-consent trackers, none persist`: N trackers loaded before any choice
  was made, and none of them was still present after rejection.
- `VIOLATION — N tracker(s) post-rejection`, followed by the tracker domains:
  these trackers loaded before consent **and** stayed after the user rejected
  cookies.
- `error: ...`: the browser could not run. Usually this means
  `playwright install chromium` has not been run, or system libraries are
  missing (see [Installation](#installation)).

`send` also runs the MailRadar and SSL checks on the publisher domain, and
includes the results in the technical measures section of the letter — but only
if that domain has been established, as described next. It does not run
CookieRadar.

## Which domain belongs to the publisher

An Android package name is the reverse-DNS of whoever *built* the app, and that
is not always who publishes it. `it.Beta80Group.whereareu` is published by AREU,
a regional health agency; reversing the package name gives the domain of Beta 80
Group, the software house that wrote it. `com.x8bit.bitwarden` gives `x8bit.com`,
a Spanish software company unrelated to Bitwarden.

Google Play's developer website field is not authoritative either: it is free
text, and for Coin Master it holds a Zendesk helpdesk tenant whose email posture
is largely Zendesk's configuration rather than the publisher's.

So APKRadar consults both, and at most one domain is ever analysed as the
publisher's:

| Situation | What APKRadar does |
|---|---|
| Both sources give the same domain | Treats it as established, labelled *package name and Google Play listing agree* |
| The listing's domain is named by the app itself | Treats it as established, labelled *from Google Play listing, and the app points at it* — see below |
| They give different domains | Uses the Google Play listing, labelled **unverified**. The reverse-DNS of the package name is set aside: not analysed, not printed |
| Only the package name gives one | Checks it, labelled *guessed from package name* — **unverified** |
| A source gives a helpdesk, site builder or social page | Leaves it out and says so: a finding there would describe the platform |
| The candidate answers with a domain-for-sale page | Leaves it out. It has no holder to attribute anything to |

**The app as a third witness.** Neither of the first two sources is independent of
the developer: the package name is their reverse-DNS and the Play listing is a
field they typed. The APK is a third thing to ask, and it answers about one host
at a time — *does this package name it, and does the mention look deliberate*. It
cannot return a domain, so it cannot introduce a target to audit; it can only
confirm one that Google Play already named.

Measured on 112 Where ARE U: the base APK names `where.areu.lombardia.it` nine
times, including the URL of its privacy notice, and `beta80group.it` — the
software house that built it — not once. Play names the first. Two parties that
did not copy from each other say the same thing, so the domain is established and
the DPO letter can state its art. 32 finding without `--publisher-domain`.

It confirms the **Play listing only**. The package name and the contents of the
APK are both the builder's work, so one corroborating the other would be a single
party speaking twice — and an `© …` line in a credits screen would put the finding
back on the supplier. And a mention has to look deliberate: a policy or terms URL,
or several occurrences. One string that happens to contain the name is not
evidence.

Where it does nothing: abc 123 Tracing mentions neither of its candidate domains
anywhere in its package, so nothing is established and the letter stays silent.

Wherever a domain is printed it carries its label, so a guess cannot be read as a
measurement — and a domain that was set aside is not printed at all. Reporting it,
even under "not analysed", puts a third party's domain in a document about someone
else's app; `beta80group.it` belongs to the software house that wrote AREU's app
and it is not a party to anything. `--verbose` shows it to whoever asks.

**A domain that is for sale is never audited.** `com.gameitech.…` reverses to
`gameitech.com`, which today redirects to a GoDaddy for-sale page: its MailRadar
score of 0/100 is the mail posture of a lander, the publisher's own `gameitech.in`
scores 14/100, and anyone can buy the name — including after a report quoting it
has been written. A row like that invalidates the audit that contains it, so the
candidate is fetched once and dropped if it answers with a sale.

In the DPO letter this matters legally: section 4 states a failing under art. 32
GDPR against the addressee. It is written only for a domain that has been
established — because both declared sources agree, because the app itself
confirms the listing, or because you passed `--publisher-domain` yourself. Otherwise the section says that the domain could
not be established and that no finding is made, which is not the same as finding
nothing. An unverified domain is never named in the letter.

Pass `--offline` to `audit --full` or `send` to skip the Google Play lookup. It
does not switch off the for-sale check, which is part of the analysis rather than
part of asking Google.

## Known limitations

- **Code obfuscation (R8/ProGuard).** Release builds are usually minified, and
  R8 can rename or move SDK classes. Code detection looks for the SDK's
  original package names. An SDK whose classes were renamed, and that declares
  no manifest component, is not detected. "No trackers detected" therefore
  means "none of the known SDKs was found". It does not mean the app contains
  no trackers.
- **Split APKs.** For `.xapk` and `.apkm` bundles only the base APK is
  analysed. Code and permissions that exist only in feature splits
  (`split_*`) or configuration splits (`config.*`) are not seen. Plain `.apk`
  files downloaded from a split install have the same limitation, because only
  that one file is analysed.
- **Static analysis only.** APKRadar does not run the app or observe its
  network traffic. It cannot tell whether an SDK is active, what it sends, or
  whether consent is collected first.
- **Declared permissions, not effective ones.** Permissions are counted as
  declared in the manifest. Restrictions such as `maxSdkVersion` are not taken
  into account. For example, the Wikipedia app declares `GET_ACCOUNTS` only up
  to Android 5.1 (`maxSdkVersion="22"`) but itself requires Android 6.0 or
  later, so the permission is never requested. APKRadar still counts it.
- **Extra-EU detection is namespace-based.** Any component under a listed
  prefix counts, including infrastructure libraries that are not trackers (see
  [Extra-EU transfers](#extra-eu-transfers)). Vendors outside the 13 listed
  prefixes are not reported.
- **Host extraction finds literals only.** A URL built by concatenation or held
  in a resource, an asset or a remote configuration is not seen, so the host list
  is a floor and not an inventory. Conversely a host that is present may never be
  contacted — the list says what the code can reach. The jurisdiction column is
  filled only for the 13 vendors above: resolving where an arbitrary endpoint is
  operated is not something a static scan can do, and a table guessing at it would
  put legal conclusions about a hundred national weather services into a letter to
  a DPO. The list stops at 500 hosts and says when it did.
- **Fixed signature lists.** Only the 43 trackers, 23 permissions and 13
  vendors listed in the code are recognised.
- **The publisher domain is still a guess, unless two sources agree.** Both
  the package name and the Play listing can be wrong, and when they disagree
  APKRadar cannot tell you which is right — it shows both, labelled. Check the
  domain yourself before relying on `--full` results, and pass
  `--publisher-domain` before sending a letter that quotes them. An SSL hostname
  mismatch is never stated as an issue in the letter.
- **Google Play lookup by package name only.** Search by app name is not
  supported. The removal-reason search depends on DuckDuckGo having a summary
  for the package, and often returns nothing.
- **DPO letter in Italian only** (see [Send a GDPR letter](#send-a-gdpr-letter-to-the-dpo)).
- **Options not yet implemented.** Two options are accepted and have no
  effect: `audit --lang` (reports are in English) and `search --audit`.

## Development

```
git clone https://github.com/maksimtech/apkradar.git
cd apkradar
pip install -e ".[dev]"
pytest
```

The changes in each release are listed in [CHANGELOG.md](CHANGELOG.md).
Security issues: see [SECURITY.md](SECURITY.md).

## How this is built

Developed with [Claude Code](https://claude.com/claude-code), reviewed and released by
[maksimtech](https://github.com/maksimtech). The suite is the contract: the release
script runs it with the new version already written and refuses to commit if it fails,
so nothing ships that it has not passed.

## License

MIT. See [LICENSE](LICENSE).
