Skip to content

0.5.0: deep scan, and a report that opens with what it could not see - #5

Merged
munzzyy merged 1 commit into
mainfrom
deep-scan-050
Sep 9, 2026
Merged

munzzyy merged 1 commit into
mainfrom
deep-scan-050

Conversation

@munzzyy

@munzzyy munzzyy commented Sep 9, 2026

Copy link
Copy Markdown
Owner

The whole release serves one directive: never wrong, and as invasive as
an unprivileged app can be. Depth and honesty ship together, because
depth without the could-not-check list would just be a longer way to
imply a verdict.

Bridge: every surface Android exposes to a sideloaded scanner is now
read, each one wrapped so a single failure cannot kill the scan, and
each reporting ok or unavailable with the concrete reason. New reads:
accessibility capability detail from AccessibilityManager (the settings
string stays as a cross-check), real runtime permission grants per app,
manifest facts (foreground service types, boot receivers, targetSdk,
lastUpdateTime, sharedUserId, debuggable), device admin policy bits via
DeviceAdminInfo, enabled IMEs, user-added CA certificates, default SMS
and dialer plus best-effort assistant, battery exemptions, device and
profile owners by per-package probe, ADB and developer switches,
install source v2 with the initiating package, and VpnService
declarers. Appop-backed specials are reported as declared only, since
the GRANTED bit lies for those. Three grant lists Android reserves for
privileged apps are hardcoded unavailable, so the report says so on
every single scan instead of staying quiet.

Analyzer: matches are tiered by evidence. Certificate match, cert plus
package, package-only with softened wording that says the cert did not
corroborate, and prefix, which never surfaces at all without a
corroborating power. Watchware (parental monitoring) is a separate
top-level dataset key, a separate matcher, a separate output array, and
a separate renderer, so no code path can hand a Family Safety install
stalkerware wording. The disguise rule is anchored on the installer,
not the name: system-style prefix plus no system record plus a non-Play
installer, because a Play-updated Google app is non-system too and must
never fire it. Every finding carries its headline template, raw
evidence, the rule id that fired, install provenance, and what the app
can do right now from the actual grant table; the UI renders findings,
it does not write them.

Dataset: fetch-indicators.py pins the exact upstream commit, ingests
watchware.yaml alongside ioc.yaml, validates certificate and package
shapes with a hard non-zero exit, prints an added and removed diff
against the committed data, and refuses a family-count swing over 20
percent without --force. The bundled data records the pinned commit
(a04c5c1958bf) and now carries aliases and certificate organizations
for recognition copy. NOTICE.md links the CC-BY 4.0 text and states the
subset is filtered and reformatted, which the license asks for and the
old notice skipped.

Tests: a labeled benign corpus (TalkBack, VoiceAccess, LastPass,
Bitwarden, Find My Device, Intune, Family Link, Wellbeing, Messenger,
Google Messages, a Play-updated Google app) holds the false-accusation
floor at zero stalkerware-tier findings; the full IOC corpus must match
100 percent at the right tier; spoof twins (wrong-cert LastPass, a
stalkerware cert on a novel package, a non-system TalkBack clone, a
watchware package) must diverge from their benign twins, so a dead rule
fails the build instead of passing quietly; the three privileged
surfaces must appear in could-not-check on every scan shape; and the
fetch validators must reject malformed data with a non-zero exit.

One honest note: Family Link is not on the upstream watchware list, so
it cannot land the dual-use track from data. The test asserts the part
that protects people, that it never lands a stalkerware tier, and
Kaspersky Safe Kids exercises the dual-use assertion instead.

The whole release serves one directive: never wrong, and as invasive as
an unprivileged app can be. Depth and honesty ship together, because
depth without the could-not-check list would just be a longer way to
imply a verdict.

Bridge: every surface Android exposes to a sideloaded scanner is now
read, each one wrapped so a single failure cannot kill the scan, and
each reporting ok or unavailable with the concrete reason. New reads:
accessibility capability detail from AccessibilityManager (the settings
string stays as a cross-check), real runtime permission grants per app,
manifest facts (foreground service types, boot receivers, targetSdk,
lastUpdateTime, sharedUserId, debuggable), device admin policy bits via
DeviceAdminInfo, enabled IMEs, user-added CA certificates, default SMS
and dialer plus best-effort assistant, battery exemptions, device and
profile owners by per-package probe, ADB and developer switches,
install source v2 with the initiating package, and VpnService
declarers. Appop-backed specials are reported as declared only, since
the GRANTED bit lies for those. Three grant lists Android reserves for
privileged apps are hardcoded unavailable, so the report says so on
every single scan instead of staying quiet.

Analyzer: matches are tiered by evidence. Certificate match, cert plus
package, package-only with softened wording that says the cert did not
corroborate, and prefix, which never surfaces at all without a
corroborating power. Watchware (parental monitoring) is a separate
top-level dataset key, a separate matcher, a separate output array, and
a separate renderer, so no code path can hand a Family Safety install
stalkerware wording. The disguise rule is anchored on the installer,
not the name: system-style prefix plus no system record plus a non-Play
installer, because a Play-updated Google app is non-system too and must
never fire it. Every finding carries its headline template, raw
evidence, the rule id that fired, install provenance, and what the app
can do right now from the actual grant table; the UI renders findings,
it does not write them.

Dataset: fetch-indicators.py pins the exact upstream commit, ingests
watchware.yaml alongside ioc.yaml, validates certificate and package
shapes with a hard non-zero exit, prints an added and removed diff
against the committed data, and refuses a family-count swing over 20
percent without --force. The bundled data records the pinned commit
(a04c5c1958bf) and now carries aliases and certificate organizations
for recognition copy. NOTICE.md links the CC-BY 4.0 text and states the
subset is filtered and reformatted, which the license asks for and the
old notice skipped.

Tests: a labeled benign corpus (TalkBack, VoiceAccess, LastPass,
Bitwarden, Find My Device, Intune, Family Link, Wellbeing, Messenger,
Google Messages, a Play-updated Google app) holds the false-accusation
floor at zero stalkerware-tier findings; the full IOC corpus must match
100 percent at the right tier; spoof twins (wrong-cert LastPass, a
stalkerware cert on a novel package, a non-system TalkBack clone, a
watchware package) must diverge from their benign twins, so a dead rule
fails the build instead of passing quietly; the three privileged
surfaces must appear in could-not-check on every scan shape; and the
fetch validators must reject malformed data with a non-zero exit.

One honest note: Family Link is not on the upstream watchware list, so
it cannot land the dual-use track from data. The test asserts the part
that protects people, that it never lands a stalkerware tier, and
Kaspersky Safe Kids exercises the dual-use assertion instead.

English and Spanish stay at full parity (246 strings). versionCode 500.
No new permissions: the manifest is still QUERY_ALL_PACKAGES and
nothing else. ACCESS_NETWORK_STATE stayed out on purpose; VPN presence
detection would need it, so the VPN surface ships as the declarer list
plus best-effort settings reads instead.

The disguise rule needs more than a non-Play installer. The Galaxy Store
carries real com.samsung apps and enterprise setups push real com.android
ones, so a system-looking name only gets a card when it also hides from
the launcher, holds a watching power, or starts on boot.
@munzzyy
munzzyy merged commit b9249ba into main Sep 9, 2026
3 checks passed
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