|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Build a SYNTHETIC ``logs/ManagedSettings`` tree (fabricated identifiers, no data from any device). |
| 3 | +
|
| 4 | +The structure mirrors what the managedSettings artifact reads in an iOS 26 and an iOS 27 sysdiagnose: |
| 5 | +``User`` and ``System`` scopes, a ``SettingRecords.plist`` and a ``DataVersion.plist`` per scope, an |
| 6 | +``EffectiveSettings.plist`` for the user scope and, per client folder, one ``<store identifier>.plist`` |
| 7 | +per store with a ``.tokenized.plist`` twin and a ``clientEffectiveSettings.plist``. App and category |
| 8 | +identifiers are ``com.example.*`` and ``SYNTH-CAT-*``; every UUID derives from a fixed namespace, so |
| 9 | +the output is reproducible. The two variants differ in the store names they hold, the way the two |
| 10 | +sysdiagnoses compared in the contribution notes do. |
| 11 | +
|
| 12 | + python synthetic_managed_settings.py ios27 out.tar.gz |
| 13 | + python synthetic_managed_settings.py ios26 out.tar.gz |
| 14 | +
|
| 15 | +``build_files(variant)`` returns {path below logs/ManagedSettings: bytes}; ``edge_files()`` returns the |
| 16 | +unusual files the tests add (an unreadable file, a store with no setting key, an unknown key). |
| 17 | +""" |
| 18 | + |
| 19 | +import io |
| 20 | +import plistlib |
| 21 | +import sys |
| 22 | +import tarfile |
| 23 | +import uuid |
| 24 | + |
| 25 | +_NS = uuid.UUID("87654321-4321-8765-4321-876543218765") |
| 26 | +TOKEN_KEY = bytes(range(32)) # fabricated key material, must never be copied |
| 27 | +BUILDS = {"ios27": "24A437", "ios26": "23G71"} |
| 28 | + |
| 29 | + |
| 30 | +def _uid(label): |
| 31 | + return str(uuid.uuid5(_NS, label)).upper() |
| 32 | + |
| 33 | + |
| 34 | +def _plist(obj): |
| 35 | + return plistlib.dumps(obj, fmt=plistlib.PlistFormat.FMT_BINARY) |
| 36 | + |
| 37 | + |
| 38 | +def _store(name, active, sync, version=3, **settings): |
| 39 | + out = {"StoreName": name, "Properties": {"active": active, "syncToWatch": sync}, "version": version} |
| 40 | + out.update(settings) |
| 41 | + return out |
| 42 | + |
| 43 | + |
| 44 | +def _restrictions(denied): |
| 45 | + return { |
| 46 | + "appStore.denyAppStoreAppInstallation": denied, |
| 47 | + "appStore.denyMarketplaceAppInstallation": False, |
| 48 | + "appStore.denyWebDistributionAppInstallation": False, |
| 49 | + "appStore.maximumRating": 200 if denied else 1000, |
| 50 | + "webContent.blockedByFilter": {"policy": 1, "neverAllow": []}, |
| 51 | + "webContent.limitWebProxies": False, |
| 52 | + "userSafety.scanningPolicy": {}, |
| 53 | + } |
| 54 | + |
| 55 | + |
| 56 | +def _intelligence(siri_denied): |
| 57 | + return { |
| 58 | + "siri.denySiri": siri_denied, |
| 59 | + "siri.denySiri3489": False, |
| 60 | + "siri.forceSiriReduceSensitiveContent": False, |
| 61 | + "intelligence.denyExternalIntelligenceIntegrations": False, |
| 62 | + "intelligence.denyGenmoji": siri_denied, |
| 63 | + "intelligence.denyImagePlayground": False, |
| 64 | + "intelligence.denyImageWand": False, |
| 65 | + "intelligence.denyPhotorealisticImageGeneration": False, |
| 66 | + "eyeRelief.forceScreenDistanceOn": False, |
| 67 | + } |
| 68 | + |
| 69 | + |
| 70 | +def _shield(index): |
| 71 | + return { |
| 72 | + "shield.applicationCategories": { |
| 73 | + "activities": [{"bundleIdentifier": f"com.example.shielded{index}"}, |
| 74 | + {"bundleIdentifier": f"com.example.shielded{index + 1}"}], |
| 75 | + "categories": [{"identifier": "SYNTH-CAT-1"}, {"identifier": "SYNTH-CAT-2"}], |
| 76 | + }, |
| 77 | + "shield.webDomainCategories": {"activities": [], "categories": [{"identifier": "SYNTH-CAT-1"}]}, |
| 78 | + "screenTime.categoryShieldPolicies": [ |
| 79 | + {"key": {"identifier": "SYNTH-CAT-1"}, "value": "warn"}, |
| 80 | + {"key": {"identifier": "SYNTH-CAT-2"}, "value": "block"}], |
| 81 | + } |
| 82 | + |
| 83 | + |
| 84 | +def _scanning(): |
| 85 | + return {"userSafety.scanningPolicy": { |
| 86 | + "onServices": ["photos", "messages"], "offServices": [], "onApplications": [], "offApplications": []}} |
| 87 | + |
| 88 | + |
| 89 | +# client folder -> [(store name, active, sync, settings)] |
| 90 | +def _layout(variant): |
| 91 | + if variant == "ios27": |
| 92 | + return { |
| 93 | + "com.apple.ScreenTimeSettings": [ |
| 94 | + ("ContentAndPrivacy", True, False, {**_intelligence(True), **_restrictions(False)}), |
| 95 | + ("Restrictions", True, False, _restrictions(True))], |
| 96 | + "com.apple.ScreenTimeSettings.TimeAllowances": [ |
| 97 | + ("defaultGroup", False, True, _shield(1)), |
| 98 | + ("downtime", False, False, _shield(3)), |
| 99 | + ("bedtime", False, False, {})], |
| 100 | + "com.apple.UserSafety": [("UserSafety", True, False, {})], |
| 101 | + "com.apple.Preferences": [("UserSafety", False, True, _scanning())], |
| 102 | + "FamilyControlsAgent": [("Default", True, False, |
| 103 | + {"managedSettings.tokenEncodingKey": TOKEN_KEY, |
| 104 | + "managedSettings.tokenDecodingKeys": [TOKEN_KEY]})], |
| 105 | + } |
| 106 | + return { |
| 107 | + "com.apple.ScreenTime": [ |
| 108 | + ("Default", True, False, {"deviceActivity.shareAcrossDevices": True}), |
| 109 | + ("SafariStore", True, False, {"safari.denyHistoryClearing": False, "safari.denyPrivateBrowsing": True}), |
| 110 | + ("UserSafetyStore", True, False, {"userSafety.scanningPolicy": {}})], |
| 111 | + "com.apple.UserSafety": [("UserSafety", True, False, { |
| 112 | + "userSafety.allowedClients": [{"bundleIdentifier": "com.example.camera"}, |
| 113 | + {"bundleIdentifier": "com.example.photos"}]})], |
| 114 | + "com.apple.Preferences": [("UserSafety", False, True, _scanning())], |
| 115 | + "com.apple.momentsd": [("DeviceActivityStore", True, False, { |
| 116 | + "deviceActivity.allowedClients": [{"bundleIdentifier": "com.example.moments"}]})], |
| 117 | + "FamilyControlsAgent": [("Default", True, False, {"deviceActivity.allowedClients": []})], |
| 118 | + } |
| 119 | + |
| 120 | + |
| 121 | +def build_files(variant): |
| 122 | + """{path below logs/ManagedSettings: bytes} for 'ios27' or 'ios26'.""" |
| 123 | + layout = _layout(variant) |
| 124 | + files = {"System/DataVersion.plist": _plist({"version": 8}), "System/SettingRecords.plist": _plist([]), |
| 125 | + "User/DataVersion.plist": _plist({"version": 8})} |
| 126 | + records = [] |
| 127 | + effective = {"version": 3} |
| 128 | + for client, stores in layout.items(): |
| 129 | + directory = {} |
| 130 | + client_settings = {"version": 3} |
| 131 | + for name, active, sync, settings in stores: |
| 132 | + identifier = _uid(f"{variant}/{client}/{name}") |
| 133 | + directory[name] = {"identifier": identifier, "version": 2, "active": active, "syncToWatch": sync} |
| 134 | + files[f"User/{client}/{identifier}.plist"] = _plist(_store(name, active, sync, **settings)) |
| 135 | + files[f"User/{client}/{identifier}.tokenized.plist"] = _plist( |
| 136 | + _store(name, active, sync, **{"managedSettings.tokenEncodingKey": TOKEN_KEY})) |
| 137 | + client_settings.update(settings) |
| 138 | + files[f"User/{client}/clientEffectiveSettings.plist"] = _plist(client_settings) |
| 139 | + effective.update({k: v for k, v in client_settings.items() if k != "version"}) |
| 140 | + records.append({"recordIdentifier": _uid(f"{variant}/{client}/record"), |
| 141 | + "containerPath": {"relative": f"file:///var/mobile/Library/example/Records/{client}"}, |
| 142 | + "client": {"containerIdentifier": client, "teamIdentifier": "Apple", "isInternal": True}, |
| 143 | + "version": 1, "storeDirectory": directory}) |
| 144 | + # a record with no store entry, and a store entry whose file is not in the archive |
| 145 | + records.append({"recordIdentifier": _uid(f"{variant}/empty"), |
| 146 | + "containerPath": {"relative": "file:///var/mobile/Library/example/Records/empty"}, |
| 147 | + "client": {"containerIdentifier": "com.example.empty", "teamIdentifier": "Apple", |
| 148 | + "isInternal": False}, |
| 149 | + "version": 1, "storeDirectory": {}}) |
| 150 | + records.append({"recordIdentifier": _uid(f"{variant}/missing"), |
| 151 | + "containerPath": {"relative": "file:///var/mobile/Library/example/Records/missing"}, |
| 152 | + "client": {"containerIdentifier": "com.example.missing", "teamIdentifier": "Apple", |
| 153 | + "isInternal": False}, |
| 154 | + "version": 1, "storeDirectory": {"Default": { |
| 155 | + "identifier": _uid(f"{variant}/missing/Default"), "version": 2, "active": True, |
| 156 | + "syncToWatch": False}}}) |
| 157 | + files["User/SettingRecords.plist"] = _plist(records) |
| 158 | + files["User/EffectiveSettings.plist"] = _plist(effective) |
| 159 | + return files |
| 160 | + |
| 161 | + |
| 162 | +def edge_files(): |
| 163 | + """Unusual inputs: an unreadable store, a store with an unknown key, and a store with an extra Properties key.""" |
| 164 | + return { |
| 165 | + f"User/com.example.broken/{_uid('broken')}.plist": b"this is not a property list", |
| 166 | + f"User/com.example.future/{_uid('future')}.plist": _plist( |
| 167 | + {"StoreName": "FutureStore", "Properties": {"active": True, "syncToWatch": False, "extra": 1}, |
| 168 | + "version": 9, "example.futureKey": {"b": [1, 2], "a": "x"}, "example.when": 5}), |
| 169 | + } |
| 170 | + |
| 171 | + |
| 172 | +def write_archive(path, variant, extra=None, with_noise=True): |
| 173 | + """Write a sysdiagnose-shaped .tar.gz; with_noise adds an AppleDouble sidecar and an out-of-place copy.""" |
| 174 | + root = f"sysdiagnose_SYNTHETIC_iPhone-OS_iPhone_{BUILDS[variant]}" |
| 175 | + files = {f"{root}/logs/ManagedSettings/{name}": data for name, data in build_files(variant).items()} |
| 176 | + for name, data in (extra or {}).items(): |
| 177 | + files[f"{root}/logs/ManagedSettings/{name}"] = data |
| 178 | + if with_noise: |
| 179 | + files[f"{root}/logs/ManagedSettings/User/._EffectiveSettings.plist"] = b"\x00\x05\x16\x07" |
| 180 | + files[f"{root}/other/ManagedSettings/User/EffectiveSettings.plist"] = _plist({"version": 3, "decoy": True}) |
| 181 | + with tarfile.open(path, "w:gz") as tar: |
| 182 | + for name in sorted(files): |
| 183 | + info = tarfile.TarInfo(name) |
| 184 | + info.size = len(files[name]) |
| 185 | + info.mtime = 1790708400 |
| 186 | + tar.addfile(info, io.BytesIO(files[name])) |
| 187 | + return path |
| 188 | + |
| 189 | + |
| 190 | +if __name__ == "__main__": |
| 191 | + if len(sys.argv) != 3 or sys.argv[1] not in BUILDS: |
| 192 | + sys.exit("usage: synthetic_managed_settings.py ios27|ios26 output.tar.gz") |
| 193 | + write_archive(sys.argv[2], sys.argv[1], edge_files()) |
0 commit comments