DS-0006
Stalkerware permission profiles
3,000 invented rows for a classification exercise: does this Android app's permission and behaviour profile look like stalkerware or like a benign app? The features are values an examiner can read from a phone extraction. Benign parental-control, remote-support, accessibility and backup apps look like stalkerware on purpose, and the set comes with a guide on what a wrong call costs.
At a glance
What the set is, how it was made, and how it may be used.
Feature ideas come from the Android developer documentation, the Coalition Against Stalkerware's description of stalkerware, and Lab reports on App Ops history (LAB-TR-0003), on where device security settings live on disk (LAB-TR-0017) and on last-use records (LAB-TR-0029). Sources are listed at the foot of the page.
- Task
- Binary classification: stalkerware (1) or benign (0)
- Device class
- None (generated rows, one per app on a device)
- OS
- Android, modelled
- Evidence type
- Synthetic feature table
- Made on
- A script (generate.py, Python standard library) with seed 20261106. No device, no app and no case data. The same seed gives the same bytes.
- Contents
- 3,000 rows in 45 columns (40 features), 18 app families, 141 app lineages. 225 stalkerware rows (7.5 percent). Train 2,105 rows (171 positive, 8.1 percent), test 895 rows (54 positive, 6.0 percent).
- Size
- 118 KB as a zip (120,610 bytes)
- Version
- 1.0
- Licence
- Data: CC BY 4.0. Code: MIT.
- Maintainer
- Daniel Addai, SectraTEK Lab
What is inside
One zip with the training and test tables, the reference answers for the test table, a data dictionary, a teaching guide, two baseline scripts, an evaluation script and the generator. Every column is something an examiner can read from a phone extraction; the dictionary says where.
stalkerware-permissions/ README.txt what, shape, traps LICENCE.txt CC BY 4.0, MIT DATA_DICTIONARY.md every column data_dictionary.csv the same, as CSV TEACHING_GUIDE.md plan, exercises, ethics data/train.csv 2,105 rows, labelled data/test.csv 895 rows, no label answers/test_answers.csv reference answers code/baseline_stdlib.py Python only code/baseline_sklearn.py optional code/evaluate.py scores predictions generator/generate.py seed 20261106
| Group | Columns | Note |
|---|---|---|
| Identifiers and group | row_id, app_id, lineage_id | Never features. lineage_id is the group for cross-validation. |
| Evidence and install | evidence_source, install_source, app_age_days | Type of extraction, installer, days since first install. |
| Requested permissions | n_perms_requested, n_dangerous_requested, req_sms, req_call_log, req_contacts, req_mic, req_camera, req_location, req_bg_location, req_all_files, req_query_all_packages, req_boot_completed, req_overlay, req_install_packages | From the manifest. Names follow the Android Manifest.permission reference. |
| Granted permissions | n_dangerous_granted, granted_sms, granted_call_log, granted_contacts, granted_mic, granted_camera, granted_location, granted_bg_location | Requested is not granted; granted is not used. |
| Roles and settings | a11y_service_declared, a11y_service_enabled, device_admin_active, notif_listener_enabled, sms_role_holder, launcher_icon_hidden, battery_opt_exempt, fgs_running | Accessibility service, device administrator, notification listener, SMS role, hidden icon. |
| Behaviour | bg_service_hours_7d, net_egress_bucket, appops_mic_count_7d, appops_camera_count_7d, appops_location_count_7d, appops_sms_count_7d, appops_last_access_hours | Background time, data sent (bucket), App Ops access counts. Counts are blank on bug reports. |
| Leakage column | prior_triage_flag | An earlier reviewer's note, set after the label. Do not train on it. |
| Label | is_stalkerware | In train.csv. For the test file it is in answers/test_answers.csv. |
The same columns are in test.csv except is_stalkerware. The data dictionary gives, for every column, its type, its values, its role and the artifact an examiner reads it from. The artifact paths are typical and differ by Android build and vendor; check them on your own test device.
Reference answers
The label of every test row is in answers/test_answers.csv, a separate file, so you can predict first and compare after. Each answer also names the app family that wrote the row and its kind, for error analysis.
| Family | Kind | Rows | In test | Label | Profile |
|---|---|---|---|---|---|
| messaging | Benign | 360 | 119 | 0 | Messaging apps: contacts, microphone, camera, often SMS |
| social | Benign | 360 | 112 | 0 | Social apps: contacts, camera, location |
| maps_navigation | Benign | 210 | 60 | 0 | Maps: background location, foreground service |
| camera_media | Benign | 240 | 75 | 0 | Camera and media editors |
| games | Benign | 390 | 107 | 0 | Games: mostly few permissions, some overlay |
| utility | Benign | 360 | 113 | 0 | Utilities: few permissions, quiet |
| banking_shopping | Benign | 240 | 66 | 0 | Banking and shopping apps |
| oem_system | Benign | 135 | 49 | 0 | Pre-installed helpers: often no icon, boot start, SMS role |
| parental_control | Benign look-alike | 105 | 35 | 0 | Location, SMS, call log, accessibility, device admin, boot start. Look-alike on purpose |
| remote_support | Benign look-alike | 60 | 18 | 0 | Accessibility, overlay, microphone, all files, large data sent. Look-alike on purpose |
| assistive_accessibility | Benign look-alike | 90 | 24 | 0 | Screen readers and switch tools: accessibility service on, microphone, notification access |
| backup_transfer | Benign look-alike | 105 | 30 | 0 | SMS, call log, contacts, all files, large data sent. Look-alike on purpose |
| device_management | Benign look-alike | 60 | 20 | 0 | Work device management: device admin, background location, installs |
| find_my_device | Benign look-alike | 60 | 13 | 0 | Anti-theft: background location, device admin, camera |
| sw_full_monitor | Stalkerware | 105 | 23 | 1 | Full monitor: sideloaded, hidden icon, accessibility, device admin, SMS, call log, microphone, location |
| sw_light_monitor | Stalkerware | 51 | 13 | 1 | Lighter monitor: SMS, location, notification access; less hiding |
| sw_disguised | Stalkerware | 33 | 9 | 1 | Disguised: icon shown under an innocent name, low data sent |
| dual_use_covert | Dual-use app used covertly | 36 | 9 | 1 | Same profile as parental control, icon hidden more often. No feature separates it |
Label 1 means an app built to watch a person without their knowledge (stalkerware, as the Coalition Against Stalkerware uses the word), or a dual-use app used that way. Whether the person knew and agreed is not something a permission table can show. That is why the set includes dual_use_covert: its profile is that of parental_control, and the label differs because of how the app was used.
Limits. The train file has no family column, to keep the task honest; the families of test rows are in the answers only. Whole lineages (one app on several devices or versions) go to one side, so the test file holds lineages the training file has not seen. Small groups move a figure a lot: dual_use_covert has only 9 test rows, so one app changes its recall by more than 10 points.
Documented traps
Each is on purpose, listed in the guide, and shows up in a number you can compute.
| Trap | Where | How to notice it |
|---|---|---|
| Leakage column | prior_triage_flag | An earlier reviewer's note, written after the label. Adding it lifts average precision from 0.87 to 0.97. At prediction time it does not exist. |
| Class imbalance | About 7 percent positive | Always saying benign scores a high accuracy on the test file and finds nothing; run it and read it. Precision falls further if the real base rate is lower (about 0.23 at 1 percent for the logistic regression baseline). |
| Benign look-alikes | Parental control, remote support, accessibility, backup, device management, find-my-device | Accessibility service on, device administrator, background location, SMS and call log access and large uploads are common to these and to stalkerware. |
| Covert use of a dual-use app | dual_use_covert | Same profile as parental control. No feature separates them; the label depends on consent. |
| Lineage clusters | lineage_id | Random cross-validation reads higher than the test file because rows of one lineage fall on both sides. Group by lineage. |
| Blank is not zero | appops_*_count_7d | Blank on every bug report row: a bug report holds a last-access dump, not the history database. |
| Ordinal as text | net_egress_bucket | none < low < medium < high < very_high sorts wrongly as text. |
| Identifiers | row_id, app_id | Random strings. Any signal in them is a bug in the split or the code. |
Three apps, read side by side
Test rows r2952 (a full-monitor stalkerware family), r0367 (a parental-control app) and r1550 (a parental-control-class app used covertly). The last two are the closest pair of look-alike and covert rows in the test file.
| Column | r2952 | r0367 | r1550 |
|---|---|---|---|
| install_source | sideload_browser | other_store | unknown |
| app_age_days | 255 | 275 | 32 |
| launcher_icon_hidden | 1 | 1 | 1 |
| a11y_service_enabled | 1 | 1 | 1 |
| device_admin_active | 1 | 1 | 0 |
| notif_listener_enabled | 1 | 0 | 0 |
| granted_sms | 1 | 1 | 1 |
| granted_call_log | 1 | 1 | 1 |
| granted_location | 1 | 1 | 1 |
| granted_bg_location | 1 | 1 | 1 |
| granted_mic | 1 | 1 | 1 |
| battery_opt_exempt | 1 | 1 | 1 |
| bg_service_hours_7d | 16.1 | 5.5 | 5.0 |
| net_egress_bucket | high | high | medium |
| appops_location_count_7d | 60 | 43 | 51 |
| appops_last_access_hours | 2 | 3 | 2 |
| prior_triage_flag | escalated | reviewed_ok | escalated |
| Reference answer | sw_full_monitor, label 1 | parental_control, label 0 | dual_use_covert, label 1 |
| Baseline score (logistic regression) | 0.98212 | 0.97319 | 0.95633 |
How to read it
The first row is the pattern the set calls a full monitor. The app was downloaded in a browser and installed outside a store, about 255 days before the extraction (it is not new on the device), with: launcher icon hidden; accessibility service on; device administrator active; notification listener on; SMS access granted; call log access granted; background location granted; microphone granted; exempt from battery optimisation. Several of these together are an indicator consistent with monitoring software; each alone is not.
The second and third rows are the point of the set. They differ in 3 of the 27 yes/no columns and the install source (other store and unknown); both are above the baseline's threshold and both are flagged. One is a parent's app and the other is the same class of app used to watch an adult. Nothing in this table says who installed the app or whether the person using the phone knew. The reference answers give label 0 and label 1, and the evidence for the difference sits outside the table.
Values are invented. The column prior_triage_flag is shown so you can see that it tracks the label; it is the leakage column.
What the shipped baselines score
Run on the test file with code/baseline_stdlib.py (Python standard library only) and scored with code/evaluate.py. The threshold is chosen on held-out training lineages, not on the test file. Two rows are not printed here, because questions 5 and 6 of the challenge ask for them: run the always-benign answer and the default logistic regression, and read the scores off.
| Model | Accuracy | Precision | Recall | F1 | Average precision | Look-alikes flagged |
|---|---|---|---|---|---|---|
| Indicator count Eight indicators, threshold chosen on train | 0.951 | 0.564 | 0.815 | 0.667 | 0.745 | 24% |
| Logistic regression, balanced weights Positive rows weighted up | 0.960 | 0.607 | 0.944 | 0.739 | 0.874 | 23% |
| Logistic regression, WITH the leakage column What not to do | 0.994 | 0.945 | 0.963 | 0.954 | 0.975 | 1% |
Reading it. Accuracy stays high for every model, while precision and recall differ a lot. The leakage row looks like progress and is not. In the logistic regression, 20 of 35 parental-control test apps (57 percent) are flagged: each is a parent's app on a child's phone. Numbers are for this synthetic set and say nothing about real devices.
Try it with free tools
- Download the zip below and check its SHA-256 (Windows:
certutil -hashfile stalkerware-permissions-1.0.zip SHA256; Linux or macOS:sha256sum). - Unzip it. Open
data/train.csvin a plain text editor, a spreadsheet, or DB Browser for SQLite (File, Import, Table from CSV file). - Count before you model. In SQLite, with the table named
train, run the query on the right. Which combinations hold most of the stalkerware rows? How many benign apps share them? - Install Python 3, open a terminal in the
codefolder and runpython baseline_stdlib.py --out p.csv, thenpython evaluate.py p.csv ../answers/test_answers.csv. - Rerun with
--with-leakand with--class-weight balanced. Explain each change in your own words. - Read
TEACHING_GUIDE.md: exercises with expected results, and the discussion on false positives. If you have scikit-learn, trybaseline_sklearn.py, which compares random and grouped cross-validation.
SELECT launcher_icon_hidden AS hidden_icon,
a11y_service_enabled AS a11y_on,
device_admin_active AS dev_admin,
COUNT(*) AS apps,
SUM(is_stalkerware) AS stalkerware
FROM train
GROUP BY 1, 2, 3
ORDER BY apps DESC;Try the challenge: Separate stalkerware from look-alikes (Practitioner).
When the wrong answer is a family member
The teaching guide works through this: who is harmed by each kind of error, why removing real stalkerware can alert the person who installed it (so safety planning comes first, see the Coalition Against Stalkerware), why no flag should go to a third party or lead to an app being removed on a score alone, and why a report should state the false-positive rate on the look-alike families next to the headline figure.
A sentence that fits the evidence
"App X is an enabled accessibility service and an active device administrator, has its launcher icon hidden, was installed from a browser download, and holds background location and SMS access. These are indicators consistent with monitoring software. They are also present in some parental-control and device-management apps. This examination did not establish who installed the app, whether the user of the device knew of it, or what it sent."
Not this
"Stalkerware found."
Get the dataset
One zip, 120,610 bytes. It holds 12 files: the tables, the reference answers, the guide, the scripts, the licences and the generator. Check it against the hash below.
Download stalkerware-permissions-1.0.zip (118 KB)
Served from this site, versioned. The manifest and the hashes are linked below.
SHA-256 of the zip
The loose files, manifest.json and SHA256SUMS.txt, sit beside the zip on this site. To rebuild every byte yourself, run python generate.py from the generator folder (Python 3, standard library only). Every other file is inside the zip.
Licence
Data: Creative Commons Attribution 4.0 (CC BY 4.0). The full text is at creativecommons.org/licenses/by/4.0 and in LICENCE.txt inside the zip. No acceptance step is needed.
- You may copy and share the files in any medium or format, and adapt them, for any purpose, including teaching, research, casework and commercial work.
- You must give credit, using the citation below, link to the licence and say if you changed the files.
- Please keep the hashes beside any copy you share, and say that the files are synthetic. Do not present them as taken from a real phone or a real case.
Code: MIT. generate.py, the baselines and evaluate.py are under the MIT licence. Keep the copyright notice with copies. The dataset is provided as is, without warranty.
Hashes
Check every file after download. The set hash is the SHA-256 of manifest.json, which lists the size and hash of every other file.
Set hash (SHA-256, manifest.json)
| File | Content | Size | SHA-256 |
|---|---|---|---|
| stalkerware-permissions-1.0.zip | Archive | 120,610 bytes | efeb5ede7162b6cb2e333d15c647460af819fc15319384276ada7441a02d1d10 |
| data/train.csv | Training table | 296,757 bytes | df43490dd308358f7142392cd1b16786622a6786f347fd9daca5d8407ba54d22 |
| data/test.csv | Test table (no label) | 124,708 bytes | 4d859579b77219045dde591a95a999e535b3ce66ddac09455afb3a246eff9b00 |
| answers/test_answers.csv | Reference answers | 30,100 bytes | 9f0b6d964ee2cc0084758767c99f681a313442ec6e230f2cc38a7943c906278f |
| data_dictionary.csv | Data dictionary (machine readable) | 5,327 bytes | 411f1a46e52bb4758eb95ac85f254eb661faff3000da1bf502fa76998b587134 |
| DATA_DICTIONARY.md | Data dictionary | 7,008 bytes | c22628e3b600e806932004aa16938067e3bf471376d724c008bf2fa257d760a0 |
| TEACHING_GUIDE.md | Teaching guide | 12,390 bytes | 19aad931e567236d38069e2d1dc532614631526435d5a6898afe67fec290de0f |
| README.txt | Notes | 2,685 bytes | f05ce19f7078285956debd360944ce3a38500796ae3e03e82aa9fd4df1c5490c |
| LICENCE.txt | Licences | 1,462 bytes | 1d3020cfd5914952d7312cba9590bde5d7da7f48ab4865663570a814744cf53e |
| code/baseline_stdlib.py | Baselines (MIT) | 8,593 bytes | 3aebcf2e0f672f98dfd8f8811987da3c2ae843528e307a194dc6e38938ef1849 |
| code/baseline_sklearn.py | Optional baseline (MIT) | 5,780 bytes | 348e7d6c57e83bf95849a7b85d8d5cdf0300204cedfa6ae11a864e0ed36e692d |
| code/evaluate.py | Evaluation (MIT) | 7,444 bytes | af087af56c6d3ca2988ffed1fbd90371351c0b9793865327fbe01b5f17ebc67d |
| generator/generate.py | Generator (MIT) | 32,014 bytes | fd34ea46f6eafb0274d219b03ab7b88d7d2160796d321a0ab2846ad71ae3e15a |
| manifest.json | Manifest (file, bytes, SHA-256) | 2,179 bytes | 3a5ced10a2dc74990be09d545cc33433daa76546c2e163596d127f342981c67c |
| SHA256SUMS.txt | The same hashes in sha256sum format | 1,192 bytes | 84ef77c8cf38a37af0a3dc35ed80c3036427f8db4edd356c5991ea7889e72121 |
The zip holds the same files as the folder (the zip itself, manifest.json and SHA256SUMS.txt excepted). Together the files are 534,268 bytes before compression. Hashes and sizes on this page are read from manifest.json.
Cite this dataset
Cite as
Addai, D., SectraTEK Lab (2026). Stalkerware permission profiles (DS-0006), version 1.0. SectraTEK Lab Datasets. https://lab.sectratek.com/datasets/stalkerware-permissions/. SHA-256 of manifest.json 3a5ced10a2dc74990be09d545cc33433daa76546c2e163596d127f342981c67c
@dataset{sectratek-lab-ds-0006,
author = {Addai, Daniel},
title = {Stalkerware permission profiles},
year = {2026},
version = {1.0},
publisher = {SectraTEK Lab},
url = {https://lab.sectratek.com/datasets/stalkerware-permissions/},
note = {DS-0006, SHA-256 of manifest.json 3a5ced10a2dc74990be09d545cc33433daa76546c2e163596d127f342981c67c}
}Sources for the features
- Coalition Against StalkerwareThe definition of stalkerware used for the label, and survivor guidance
- Android permissions overviewInstall-time, runtime and special permissions; the Manifest.permission reference lists each constant
- Android accessibility servicesBIND_ACCESSIBILITY_SERVICE and what such a service can read and do
- Google Play: use of the AccessibilityService APILimits on non-accessibility apps that use the API
- Android DeviceAdminReceiverDevice administration
- Android NotificationListenerServiceNotification access
- Android AppOpsManagerThe platform record of accesses to protected operations
Related
- Report: Where Android records who used whatApp Ops history in databases, XML files and bug reports
- White paper: Where device security settings live on diskDevice administrators, runtime permissions and other settings, with paths
- Report: Can this phone answer this question?Why a last-use value is not a history
- Dataset: Synthetic recovery-history setAnother seeded set with reference answers
Datasets carry their own licence. This one is CC BY 4.0 for data and MIT for code. Articles are CC BY 4.0. All licences.