1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, thiserror::Error)]
5pub enum UsageScanError {
6 #[error("Failed to read executable: {0}")]
7 Io(#[from] std::io::Error),
8 #[error("Unable to resolve app executable")]
9 MissingExecutable,
10}
11
12#[derive(Debug, Default)]
13pub struct UsageScan {
14 pub required_keys: HashSet<&'static str>,
15 pub requires_location_key: bool,
16 pub evidence: HashSet<&'static str>,
17}
18
19#[derive(Debug, Default)]
20pub struct CapabilityScan {
21 pub detected: HashSet<&'static str>,
22 pub evidence: HashSet<&'static str>,
23}
24
25pub fn scan_usage_from_app_bundle(app_bundle_path: &Path) -> Result<UsageScan, UsageScanError> {
26 let executable =
27 resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
28 scan_usage_from_executable(&executable)
29}
30
31#[derive(Debug, Default)]
32pub struct PrivateApiScan {
33 pub hits: Vec<&'static str>,
34}
35
36#[derive(Debug, Default)]
37pub struct SdkScan {
38 pub hits: Vec<&'static str>,
39}
40
41pub fn scan_private_api_from_app_bundle(
42 app_bundle_path: &Path,
43) -> Result<PrivateApiScan, UsageScanError> {
44 let executable =
45 resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
46 scan_private_api_from_executable(&executable)
47}
48
49pub fn scan_sdks_from_app_bundle(app_bundle_path: &Path) -> Result<SdkScan, UsageScanError> {
50 let executable =
51 resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
52 scan_sdks_from_executable(&executable)
53}
54
55pub fn scan_capabilities_from_app_bundle(
56 app_bundle_path: &Path,
57) -> Result<CapabilityScan, UsageScanError> {
58 let executable =
59 resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
60 scan_capabilities_from_executable(&executable)
61}
62
63fn resolve_executable_path(app_bundle_path: &Path) -> Option<PathBuf> {
64 let app_name = app_bundle_path
65 .file_name()
66 .and_then(|n| n.to_str())
67 .unwrap_or("")
68 .trim_end_matches(".app");
69
70 if app_name.is_empty() {
71 return None;
72 }
73
74 let executable_path = app_bundle_path.join(app_name);
75 if executable_path.exists() {
76 Some(executable_path)
77 } else {
78 None
79 }
80}
81
82fn scan_usage_from_executable(path: &Path) -> Result<UsageScan, UsageScanError> {
83 let bytes = std::fs::read(path)?;
84 let mut scan = UsageScan::default();
85
86 for (signature, requirement) in SIGNATURES {
87 if contains_subslice(&bytes, signature.as_bytes()) {
88 scan.evidence.insert(*signature);
89 match requirement {
90 Requirement::Key(key) => {
91 scan.required_keys.insert(*key);
92 }
93 Requirement::AnyLocation => {
94 scan.requires_location_key = true;
95 }
96 }
97 }
98 }
99
100 Ok(scan)
101}
102
103fn scan_private_api_from_executable(path: &Path) -> Result<PrivateApiScan, UsageScanError> {
104 let bytes = std::fs::read(path)?;
105 let mut scan = PrivateApiScan::default();
106
107 for signature in PRIVATE_API_SIGNATURES {
108 if contains_subslice(&bytes, signature.as_bytes()) {
109 scan.hits.push(*signature);
110 }
111 }
112
113 scan.hits.sort_unstable();
114 scan.hits.dedup();
115
116 Ok(scan)
117}
118
119fn scan_sdks_from_executable(path: &Path) -> Result<SdkScan, UsageScanError> {
120 let bytes = std::fs::read(path)?;
121 let mut scan = SdkScan::default();
122
123 for signature in SDK_SIGNATURES {
124 if contains_subslice(&bytes, signature.as_bytes()) {
125 scan.hits.push(*signature);
126 }
127 }
128
129 scan.hits.sort_unstable();
130 scan.hits.dedup();
131
132 Ok(scan)
133}
134
135fn scan_capabilities_from_executable(path: &Path) -> Result<CapabilityScan, UsageScanError> {
136 let bytes = std::fs::read(path)?;
137 let mut scan = CapabilityScan::default();
138
139 for (signature, capability) in CAPABILITY_SIGNATURES {
140 if contains_subslice(&bytes, signature.as_bytes()) {
141 scan.evidence.insert(*signature);
142 scan.detected.insert(*capability);
143 }
144 }
145
146 Ok(scan)
147}
148
149fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
150 haystack
151 .windows(needle.len())
152 .any(|window| window == needle)
153}
154
155#[derive(Clone, Copy)]
156enum Requirement {
157 Key(&'static str),
158 AnyLocation,
159}
160
161const SIGNATURES: &[(&str, Requirement)] = &[
162 (
163 "AVCaptureDevice",
164 Requirement::Key("NSCameraUsageDescription"),
165 ),
166 (
167 "AVAudioSession",
168 Requirement::Key("NSMicrophoneUsageDescription"),
169 ),
170 (
171 "AVAudioRecorder",
172 Requirement::Key("NSMicrophoneUsageDescription"),
173 ),
174 (
175 "PHPhotoLibrary",
176 Requirement::Key("NSPhotoLibraryUsageDescription"),
177 ),
178 (
179 "PHPhotoLibraryAddOnly",
180 Requirement::Key("NSPhotoLibraryAddUsageDescription"),
181 ),
182 ("CLLocationManager", Requirement::AnyLocation),
183 (
184 "CBCentralManager",
185 Requirement::Key("NSBluetoothAlwaysUsageDescription"),
186 ),
187 (
188 "CBPeripheralManager",
189 Requirement::Key("NSBluetoothAlwaysUsageDescription"),
190 ),
191 (
192 "CBPeripheral",
193 Requirement::Key("NSBluetoothPeripheralUsageDescription"),
194 ),
195 ("LAContext", Requirement::Key("NSFaceIDUsageDescription")),
196 (
197 "EKEventStore",
198 Requirement::Key("NSCalendarsUsageDescription"),
199 ),
200 (
201 "EKReminder",
202 Requirement::Key("NSRemindersUsageDescription"),
203 ),
204 (
205 "CNContactStore",
206 Requirement::Key("NSContactsUsageDescription"),
207 ),
208 (
209 "SFSpeechRecognizer",
210 Requirement::Key("NSSpeechRecognitionUsageDescription"),
211 ),
212 (
213 "CMMotionManager",
214 Requirement::Key("NSMotionUsageDescription"),
215 ),
216 ("CMPedometer", Requirement::Key("NSMotionUsageDescription")),
217 (
218 "MPMediaLibrary",
219 Requirement::Key("NSAppleMusicUsageDescription"),
220 ),
221 (
222 "HKHealthStore",
223 Requirement::Key("NSHealthShareUsageDescription"),
224 ),
225];
226
227const PRIVATE_API_SIGNATURES: &[&str] = &[
228 "LSApplicationWorkspace",
229 "LSApplicationProxy",
230 "LSAppWorkspace",
231 "SBApplication",
232 "SpringBoard",
233 "MobileGestalt",
234 "UICallApplication",
235 "UIGetScreenImage",
236 "_MGCopyAnswer",
237];
238
239const SDK_SIGNATURES: &[&str] = &[
240 "FirebaseApp",
241 "FIRApp",
242 "GADMobileAds",
243 "FBSDKCoreKit",
244 "FBSDKLoginKit",
245 "Amplitude",
246 "Mixpanel",
247 "Segment",
248 "SentrySDK",
249 "AppsFlyerLib",
250 "Adjust",
251];
252
253const CAPABILITY_SIGNATURES: &[(&str, &str)] = &[
254 ("AVCaptureDevice", "camera"),
255 ("CLLocationManager", "location"),
256];