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
36pub fn scan_private_api_from_app_bundle(
37 app_bundle_path: &Path,
38) -> Result<PrivateApiScan, UsageScanError> {
39 let executable =
40 resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
41 scan_private_api_from_executable(&executable)
42}
43
44pub fn scan_capabilities_from_app_bundle(
45 app_bundle_path: &Path,
46) -> Result<CapabilityScan, UsageScanError> {
47 let executable =
48 resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
49 scan_capabilities_from_executable(&executable)
50}
51
52fn resolve_executable_path(app_bundle_path: &Path) -> Option<PathBuf> {
53 let app_name = app_bundle_path
54 .file_name()
55 .and_then(|n| n.to_str())
56 .unwrap_or("")
57 .trim_end_matches(".app");
58
59 if app_name.is_empty() {
60 return None;
61 }
62
63 let executable_path = app_bundle_path.join(app_name);
64 if executable_path.exists() {
65 Some(executable_path)
66 } else {
67 None
68 }
69}
70
71fn scan_usage_from_executable(path: &Path) -> Result<UsageScan, UsageScanError> {
72 let bytes = std::fs::read(path)?;
73 let mut scan = UsageScan::default();
74
75 for (signature, requirement) in SIGNATURES {
76 if contains_subslice(&bytes, signature.as_bytes()) {
77 scan.evidence.insert(*signature);
78 match requirement {
79 Requirement::Key(key) => {
80 scan.required_keys.insert(*key);
81 }
82 Requirement::AnyLocation => {
83 scan.requires_location_key = true;
84 }
85 }
86 }
87 }
88
89 Ok(scan)
90}
91
92fn scan_private_api_from_executable(path: &Path) -> Result<PrivateApiScan, UsageScanError> {
93 let bytes = std::fs::read(path)?;
94 let mut scan = PrivateApiScan::default();
95
96 for signature in PRIVATE_API_SIGNATURES {
97 if contains_subslice(&bytes, signature.as_bytes()) {
98 scan.hits.push(*signature);
99 }
100 }
101
102 scan.hits.sort_unstable();
103 scan.hits.dedup();
104
105 Ok(scan)
106}
107
108fn scan_capabilities_from_executable(path: &Path) -> Result<CapabilityScan, UsageScanError> {
109 let bytes = std::fs::read(path)?;
110 let mut scan = CapabilityScan::default();
111
112 for (signature, capability) in CAPABILITY_SIGNATURES {
113 if contains_subslice(&bytes, signature.as_bytes()) {
114 scan.evidence.insert(*signature);
115 scan.detected.insert(*capability);
116 }
117 }
118
119 Ok(scan)
120}
121
122fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
123 haystack
124 .windows(needle.len())
125 .any(|window| window == needle)
126}
127
128#[derive(Clone, Copy)]
129enum Requirement {
130 Key(&'static str),
131 AnyLocation,
132}
133
134const SIGNATURES: &[(&str, Requirement)] = &[
135 (
136 "AVCaptureDevice",
137 Requirement::Key("NSCameraUsageDescription"),
138 ),
139 (
140 "AVAudioSession",
141 Requirement::Key("NSMicrophoneUsageDescription"),
142 ),
143 (
144 "AVAudioRecorder",
145 Requirement::Key("NSMicrophoneUsageDescription"),
146 ),
147 (
148 "PHPhotoLibrary",
149 Requirement::Key("NSPhotoLibraryUsageDescription"),
150 ),
151 (
152 "PHPhotoLibraryAddOnly",
153 Requirement::Key("NSPhotoLibraryAddUsageDescription"),
154 ),
155 ("CLLocationManager", Requirement::AnyLocation),
156 (
157 "CBCentralManager",
158 Requirement::Key("NSBluetoothAlwaysUsageDescription"),
159 ),
160 (
161 "CBPeripheralManager",
162 Requirement::Key("NSBluetoothAlwaysUsageDescription"),
163 ),
164 (
165 "CBPeripheral",
166 Requirement::Key("NSBluetoothPeripheralUsageDescription"),
167 ),
168 ("LAContext", Requirement::Key("NSFaceIDUsageDescription")),
169 (
170 "EKEventStore",
171 Requirement::Key("NSCalendarsUsageDescription"),
172 ),
173 (
174 "EKReminder",
175 Requirement::Key("NSRemindersUsageDescription"),
176 ),
177 (
178 "CNContactStore",
179 Requirement::Key("NSContactsUsageDescription"),
180 ),
181 (
182 "SFSpeechRecognizer",
183 Requirement::Key("NSSpeechRecognitionUsageDescription"),
184 ),
185 (
186 "CMMotionManager",
187 Requirement::Key("NSMotionUsageDescription"),
188 ),
189 ("CMPedometer", Requirement::Key("NSMotionUsageDescription")),
190 (
191 "MPMediaLibrary",
192 Requirement::Key("NSAppleMusicUsageDescription"),
193 ),
194 (
195 "HKHealthStore",
196 Requirement::Key("NSHealthShareUsageDescription"),
197 ),
198];
199
200const PRIVATE_API_SIGNATURES: &[&str] = &[
201 "LSApplicationWorkspace",
202 "LSApplicationProxy",
203 "LSAppWorkspace",
204 "SBApplication",
205 "SpringBoard",
206 "MobileGestalt",
207 "UICallApplication",
208 "UIGetScreenImage",
209 "_MGCopyAnswer",
210];
211
212const CAPABILITY_SIGNATURES: &[(&str, &str)] = &[
213 ("AVCaptureDevice", "camera"),
214 ("CLLocationManager", "location"),
215];