Repository navigation
Releases: maksimtech/apkradar
Release list
APKRadar 2026.45
Fixed
- A manifest component whose class is not in the DEX is no longer a tracker, nor a
transfer. Fennec 157.0.0 as F-Droid builds it (org.mozilla.fennec_fdroid1570020,
SHA-25604a5f4d3…47a9) declarescom.adjust.sdk.AdjustPreinstallReferrerReceiver
and holds no class undercom.adjustin any of its three DEX files: the build is made
without the Adjust SDK and the manifest keeps the receiver. The audit reported "Adjust"
and a transfer to "Adjust GmbH (Germany) → USA", and the DPO letter would have put both
in front of Mozilla — an allegation about code that is not in the file. Declared
components are now confirmed against the DEX (Lcom/adjust/sdk/Adjust…Receiver;,
the whole descriptor) before they count; an APK with no readable code is still read on
the manifest's word, since "nothing to check against" is not "confirmed absent".
tests/test_declared_without_code.pyholds the manifest excerpt as a real APK. --fullno longer audits every deep-link host of an app that opens other sites'
links. OsmAnd~ 5.4.9 (net.osmand.plus540903) declares 427 deep-link hosts —
maps.google.com and some 200 Google country domains, map.baidu.com, maps.yandex.ru,
here.com, maps.apple.com — because it opens links to other maps; NewPipe 0.29.1 declares
56 for YouTube, SoundCloud, Bandcamp and PeerTube instances. Each was taken as "SDK or
deep-link domain found in the APK" and queued for MailRadar, a TLS handshake and a
headless browser: on OsmAnd about three and a half hours, at the ~30 s per domain
measured on Mastodon the same day, of traffic to Google, Baidu and Yandex about an app
that talks to none of them. AboveMAX_DEEP_LINK_DOMAINS(10; the most a publisher was
seen declaring for itself is F-Droid's four) no deep-link host is audited, the report
says how many were set aside and why, and the hosts stay inScanResult.manifest_domains
as data.tests/test_deep_links_cap.pyholds 24 of OsmAnd's hosts as a real APK.- Namespace URIs and RFC 2606 names are references, not endpoints — and not vendors.
Measured on 2026-10-09: ExoPlayer's PlayReady code carries
http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense, the SOAPAction of a
licence request, and the hosts block listed it under "a vendor this tool reports on" as
Microsoft Corporation (USA) on Nextcloud 35.0.1, AntennaPod 3.12.2, NewPipe 0.29.1 and
Fennec 157.0.0 alike — none of which talks to Microsoft, and a reader takes that table
for a transfer.ns.adobe.com(the XMP namespace),xml.organd
javax.xml.XMLConstants(parser feature names) andspdx.org(licence identifiers)
join the reference list for the same reason. So do the names RFC 2606 reserves for
documentation: F-Droid 2.0.1 carrieshttps://mirror.example.com/fdroid/repoas a
sample address, and example.com, example.net, example.org, threemirror.example.*and
dummy.examplewere listed as endpoints the code can reach. They resolve to nothing and
belong to nobody. All of them stay in the list, under "references", as the block has
always done with a specification URI.tests/test_hosts_identifiers.pyholds the
literals as they appear in those DEX files. - A
batch-excelrow that was not analysed keeps its name and shows no counts.
Measured on 2026-10-09 with a registry of eleven F-Droid APKs and one row naming an app
whose file was not there: the report wrote that row asNone | None | APK | … | N/A | 0 | 0 | 0. The name and package the registry gave it were gone — a scan that could not open
the file knows neither, and the row was built from the result rather than from the
registry — so the one line a reader most needs to identify was the one that could not
be. "APK" was the dataclass default, not a reading of the file, and three zeros in the
columns that get summed and charted said the file had been found to hold nothing, when
it had not been found. The score cell was already blank for exactly that reason; the
format and the counts now are too, in both the report and--augmentmode, and the
SKIPPED row (package name, no file) loses its zeros the same way.
tests/test_excel_not_measured.pyruns the command on a real registry. audit --outputrefuses a target it cannot write before the scan, without a
traceback. Measured on 2026-10-09 in the published image,docker run … -v apk:/data:ro … audit /data/fennec.apk --output /data/x.htmlran the whole audit, printed
it, and ended in a Rich traceback throughconsole.save_html—OSError: [Errno 30] Read-only file system. The target was checked for its extension and for its parent
directory existing, and a read-only directory exists. So does a directory that happens
to carry the report's name. Both are now refused up front with exit code 2 and one line,
which is the point of checking the target before the APK is opened.
tests/test_report_target_writable.py.- Report tables fold long identifiers instead of cutting them. Measured on 2026-10-09:
audit fennec.apk --output report.htmlthrough a pipe — how a report is saved from a
script, and where Rich settles on 80 columns — wrote the tracker row as
com.google.android.gms.ads.identifi…, and the saved HTML heldidentifi…and nowhere
the stringads.identifier. Rich's default for a cell that does not fit is an ellipsis;
the package column is the finding, and a report file does not know how wide the terminal
was. The package, permission, prefix and host columns now fold onto the next line.
tests/test_report_identifiers_whole.pyrenders at 80 and 56 columns. - The README's counts are the code's. It said "43 SDKs", "24 Android permissions" and,
in Known limitations, "43 trackers, 23 permissions": the code recognises 44 SDK names
over 46 signatures and 28 permissions, and the permissions table left out the four
advertising identifiers (AD_IDand the three Privacy Sandbox permissions) added on
2026-09-25.tests/test_readme_counts.pynow reads the numbers and the table from the
README and compares them withscanner.py, so the next addition fails a test instead of
ageing the document. The limitation also names what the list does not have, measured
against εxodus (432 trackers on 2026-10-09): Glean and ACRA are the two found in the
F-Droid apps audited that day.
Changed
- mailradar 2026.44 and cookieradar 2026.44 are now the floor. Both carry the fixes their own real-world runs found — GPG credited only when the keyserver verifies the address, all 33 DKIM selectors asked; TrustArc and Usercentrics refusals clicked, an error page in any session reported as NOT MEASURED — and apkradar's audit leans on those checks.
pip install -U apkradarbrings them along.
APKRadar 2026.44.1
Changed
- The suite also runs on Python 3.15-dev, as a row that may fail. 3.15 goes final
on 2026-10-09 (PEP 790). The classifiers and the stable matrix stay at 3.11-3.14 and
tests/test_ci_dependencies.pynow checks that they are the same list, with the
experimental row being the version after the last one. A dependency without a wheel
for 3.15 shows up as a yellow row before the release rather than as a red matrix
after the classifier is added; mailradar and patchradar have had the row since
3.14-dev.
Security
-
The image no longer installs
gnupganddefault-jre-headless, which nothing in it
ran. Both stood in the Dockerfile from the first commit (2026-09-12; the JRE as
openjdk-17-jre-headlessuntil trixie stopped shipping it). No code in apkradar
executes a system binary: the APK is read by androguard, which is pure Python; the DPO
letter goes out over SMTP from this package's own sender;mailradar.checker, the one
mailradar entry point apkradar calls, looks GPG keys up over HTTP through
mailradar.gpg, andcookieradar.scannerruns no Java. Thegpgbinary is run by
mailradar.sender, which apkradar never imports.What
gnupgdid bring in was dirmngr → libldap2 → libsasl2-2, and Docker Scout
reported CVE-2026-107161 (high) in cyrus-sasl2 against it — "not fixed" in trixie, open
in every Debian suite according topatchradar debian, so no rebuild would ever have
closed it.libsasl2-2is not inpython:3.12-slim-trixieitself, measured with
apt-get install -s gnupgin the base image: it arrives only withgnupg, and leaves
with it. The apt step is nowupdate && upgrade, as in exeradar and patchradar.Measured on the two images: 913 MB → 612 MB (
docker images, 301 MB less), 117 → 87
Debian packages, anddocker scout cves --only-package cyrus-sasl2goes from one high
to no package at all.tests/test_docker_contract.pynow parses the apt step and
fails on any package it names, andtests/docker/inspect.sh, run against the built
image in CI, fails iflibsasl2-2,gnupgordefault-jre-headlessis installed or
gpgorjavais on PATH — the one judgement in a script that otherwise only reports.
The smoke test,--help,--versionand anauditof a real APK were run inside the
new image before this was written down.
APKRadar 2026.44
Added
-
The files the build is told to include are checked to be there. apkradar lost its
LICENSEout of the working tree on 2026-10-04 and the loss reachedmain: pyproject
names the file, sopython -m buildfailed withLicense file does not exist: LICENSE,
and the PyPI publish and the image went down with it. cookieradar lost its own a few
hours later, during a run of the suite. Neither suite noticed, because neither looked.What removes them is still not known, and these cases do not explain it. They stop it
reaching a commit, which is the part that can be fixed without knowing.The expectation is read out of the declarations rather than written down as
LICENSE,
because the five Radar do not declare it the same way: patchradar states its licence as
text and only its Dockerfile names the file, the other four name it in pyproject, and of
those apkradar and mailradar do not copy it into the image. So two cases — every file
pyproject names, and every path the Dockerfile copies — and between them each repository
is covered, three of them twice.Checked by moving the file aside in all five: it fails where it should and passes where
the declaration genuinely does not name it, and pointing pyproject at a file that is not
there fails too.
Fixed
-
The OCI licence label is the key the standard names. It read
org.opencontainers.image.license, singular, which nothing reads — so a tool asking the
image what it is licensed under got no answer, while the label looked right in the file.
cookieradar's suite has rejected that spelling for a while; this one had not been asked. -
Two defences in
release.shthat the tests did not actually measure. Found by
mutating the script rather than by reading it.Replacing the existing-tag check's
failwith anechoof the same words left every
case green: the script carried on, bumped, committed, and only then didgit tag
refuse the tag that already existed. The release was still refused — one commit too
late, which is the opposite of what the script promises, that a refusal leaves the
version file modified and nothing else. The cases now check that it did not commit on
its way to refusing.And
git push --atomicwas not measured at all; two separate pushes passed. With two
pushesmainarrives and the tag does not, so the repository carries a version bump
that no release and no published artifact corresponds to — and the tag that would
produce them cannot be pushed afterwards either, because the version it would be
given is by then "already the current version". Apre-receivehook on the test
remote now refuses tags, which is the way to make the second half fail on demand.Both mutations fail now, along with the three that already did.
-
The PyPI wait allows a margin once the index answers. apkradar's Docker build
failed on 2026-10-03 withNo matching distribution foundfifteen seconds after
the wait had reported the version available — 16:31:21 against 16:31:36. The poll is
not wrong and not enough: it establishes that the file is reachable from the runner,
while the build container, multi-platform through buildx, resolves the index again
and can reach an edge still serving the old one.This is not the
sleep 60that stood in that step before polling and lost the race
twice. That was a guess about how long publishing takes, made before knowing
anything; this waits for the fact first and then allows a bounded margin for it to
propagate, and says so in the log when it uses one.It narrows the window; it does not close it. What closes it is not asking the index
during the build at all — which is what exeradar's Dockerfile already does, with
pip install /app/src, and why exeradar has no wait script and did not hit this.
cookieradar and patchradar are one word from that (_SOURCE=local); apkradar and
mailradar would need the build argument added. That follow-up is the first entry
under Changed, below.Three cases hold the margin, and they were needed twice over. The two cases that
already drove this script pass the retry interval as zero so they stay fast, and the
new default made every success wait 45 seconds past their timeout — so the suite was
red in all four repositories until those two were told to ask for no grace. Telling
them that alone would have left the margin itself unmeasured, which is the shape of
defect this script was written to fix in the first place. So: one case times a
two-second grace and checks the log says why it waited, one checks that no grace
waits for nothing and claims nothing, and one measures the default without the
suite paying 45 seconds for it — started with no grace argument, the script must
still be running three seconds after the index answered. All four ways of undoing
the margin were checked against them: the default set to zero, the wait removed
while the log still claims it, the log removed while the wait still happens, and the
guard removed so zero waits anyway. -
Deep links from the manifest are read on real APKs. androguard 4 returns the
manifest as bytes, the search forandroid:hostwas a str pattern, and the TypeError
went into anexcept Exception: pass— somanifest_domainswas empty on every APK,
and a host likewhere.areu.lombardia.itnever reachedaudit --fullor
batch --full. The suite did not notice because its mocks returned a str, which is
what they return no longer. -
A deep-link host has to be a hostname, and so does what is sent to a proxy. Only
localhost,127.0.0.1and192.*were refused:10.0.2.2(the emulator's host),
other private addresses, values with spaces and values with a CR/LF went on to
MailRadar, the certificate check and theCONNECTline, which wrote the domain in
unchecked — a header of the APK's choosing, sent to the proxy. Latent while the deep
links were never read, which is why the two were fixed together. The DEX scan's
hostname rules now apply to the manifest too, and_open_tunnelrefuses anything
outside[A-Za-z0-9.-]before connecting. -
batch-excel --augmentwrites each result on the row it came from. Blank rows
were skipped when reading and not when writing, so one blank line moved every later
score, grade and tracker list onto the app above — in the file being overwritten.
Both sides now use the first sheet, as the README says, rather than whichever sheet
the file was last saved on. -
batch-excelno longer calls a failed scan GOOD. Its own chain of ifs fell
through to "🟢 GOOD — N/A — 0 trackers" for a file that was never opened; it prints
batch's line now, sensitive permissions included. -
batch --outputreports start empty. A failed scan saves no report, but stopping
the recording did not clear Rich's buffer, and its output opened the next APK's file. -
Corroboration by the APK checks the whole host.
example.com.brcounted as a
mention ofexample.com, andhttps://example.com.attacker.net/privacyor
https://example.com@attacker.net/privacyas its policy page — enough to make the
domain verified and the letter state an art. 32 finding against it. And the commonest
case could never corroborate: Play'shttps://www.example.comis normalised to
example.com, and the samewww.host in the APK was refused for the dot before it.
www.is the one prefix taken as the same host. -
The letter is in one language. Only the Italian template exists, and with
--lang enthe art. 9 reasons and the TLP block were still written in English inside
it. The language is now the template's.--langis also matched against the
templates that exist before it reaches a path: on Windows/../..used to load any
.txtas a Jinja template. -
Smaller ones.
BODY_SENSORS_BACKGROUNDis a sensitive permission, so its art. 9
reason, which law_checker always had, can be reached. A DEX with exactly 500 hosts
is no longer reported as stopped at 500.NO_PROXY=*means no proxy, as it does to
curl, requests and httpx.tlp.subjectdoes not takeTLP:AMBER+STRICTfor
TLP:AMBER. Report names inbatchare compared casefolded, soApp.apkand
app.apkno longer overwrite each other on Windows and macOS. A failed extraction
from an XAPK or APKM removes its temporary directory.com.my_company.appmaps to
my-company.com, not to a name with an underscore, anddomain_to_urlno longer
takeshttpbin.orgfor a URL.python -m apkradar.cli batch-excelexists: the
__main__block ran before that command was defined. -
docker.yml no longer pastes the dispatch input into a script. The version typed
into the form was substituted into bash, and from there into Python, in the job that
holds the Docker Hub token. It arrives throughenv:now and has to look like a
release tag; the unusedNORMALIZEDoutput went with it. A case reads every workflow
for the pattern. -
Shell scripts are LF on every clone. With Git for Windows'
core.autocrlf=true,
release.shcame out asset -euo pipefail\rand bash refused it, which a bash on
Linux reading that checkout reports as 23 failing release cases..gitattributes
now says*.sh text eol=lf, and a case asks git that it does.
Changed
- mailradar 2026.43 and cookieradar 2026.43 are now the floor. The audit leans on their checks — SPF, DMARC and DKIM on the publisher's domain, the cookie sessions on its site — and both shipped the fixes from the same review today: a bare
allread as+all,redirect=followed, the ten-lookup limit, TLS verified before the password is sent;batchsurviving a 403, the accept selector no longer clicking "disagree".pip install -U apkradarbrings them along. - The race with PyPI is closed rather than narrowed, and two stale defaults went with
it. This is the repository ...
v2026.43
Full Changelog: v2026.42...v2026.43
v2026.42
What's Changed
- feat(hosts): report the endpoints an APK carries in its DEX by @maksimtech in #16
- fix(ci): a fixed tag for the image Snyk scans by @maksimtech in #17
- ci: pin the runners to ubuntu-26.04, benchmarks to 24.04 by @maksimtech in #18
- fix(ci): mutation testing runs again — also_copy for what the tests read by @maksimtech in #19
Full Changelog: v2026.41...v2026.42
v2026.41
What's Changed
- build(deps): bump actions/upload-artifact from 4 to 7 by @dependabot[bot] in #15
Full Changelog: v2026.40...v2026.41
v2026.40
Full Changelog: v2026.09.32...v2026.40
v2026.09.32
What's Changed
- build(deps): bump docker/setup-qemu-action from 3 to 4 by @dependabot[bot] in #14
- build(deps): bump docker/login-action from 3 to 4 by @dependabot[bot] in #13
- build(deps): bump SonarSource/sonarqube-scan-action from 6 to 7 by @dependabot[bot] in #12
- build(deps): bump actions/setup-python from 5 to 7 by @dependabot[bot] in #11
- build(deps): bump codecov/codecov-action from 4 to 7 by @dependabot[bot] in #10
Full Changelog: v2026.09.31...v2026.09.32
v2026.09.31
Full Changelog: v2026.09.30...v2026.09.31
v2026.09.30
Full Changelog: v2026.09.29...v2026.09.30