verifyos_cli/parsers/
macho_scanner.rs1use 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
19pub fn scan_usage_from_app_bundle(app_bundle_path: &Path) -> Result<UsageScan, UsageScanError> {
20 let executable =
21 resolve_executable_path(app_bundle_path).ok_or(UsageScanError::MissingExecutable)?;
22 scan_usage_from_executable(&executable)
23}
24
25fn resolve_executable_path(app_bundle_path: &Path) -> Option<PathBuf> {
26 let app_name = app_bundle_path
27 .file_name()
28 .and_then(|n| n.to_str())
29 .unwrap_or("")
30 .trim_end_matches(".app");
31
32 if app_name.is_empty() {
33 return None;
34 }
35
36 let executable_path = app_bundle_path.join(app_name);
37 if executable_path.exists() {
38 Some(executable_path)
39 } else {
40 None
41 }
42}
43
44fn scan_usage_from_executable(path: &Path) -> Result<UsageScan, UsageScanError> {
45 let bytes = std::fs::read(path)?;
46 let mut scan = UsageScan::default();
47
48 for (signature, requirement) in SIGNATURES {
49 if contains_subslice(&bytes, signature.as_bytes()) {
50 scan.evidence.insert(*signature);
51 match requirement {
52 Requirement::Key(key) => {
53 scan.required_keys.insert(*key);
54 }
55 Requirement::AnyLocation => {
56 scan.requires_location_key = true;
57 }
58 }
59 }
60 }
61
62 Ok(scan)
63}
64
65fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
66 haystack
67 .windows(needle.len())
68 .any(|window| window == needle)
69}
70
71#[derive(Clone, Copy)]
72enum Requirement {
73 Key(&'static str),
74 AnyLocation,
75}
76
77const SIGNATURES: &[(&str, Requirement)] = &[
78 (
79 "AVCaptureDevice",
80 Requirement::Key("NSCameraUsageDescription"),
81 ),
82 (
83 "AVAudioSession",
84 Requirement::Key("NSMicrophoneUsageDescription"),
85 ),
86 (
87 "AVAudioRecorder",
88 Requirement::Key("NSMicrophoneUsageDescription"),
89 ),
90 (
91 "PHPhotoLibrary",
92 Requirement::Key("NSPhotoLibraryUsageDescription"),
93 ),
94 (
95 "PHPhotoLibraryAddOnly",
96 Requirement::Key("NSPhotoLibraryAddUsageDescription"),
97 ),
98 ("CLLocationManager", Requirement::AnyLocation),
99 (
100 "CBCentralManager",
101 Requirement::Key("NSBluetoothAlwaysUsageDescription"),
102 ),
103 (
104 "CBPeripheralManager",
105 Requirement::Key("NSBluetoothAlwaysUsageDescription"),
106 ),
107 (
108 "CBPeripheral",
109 Requirement::Key("NSBluetoothPeripheralUsageDescription"),
110 ),
111 ("LAContext", Requirement::Key("NSFaceIDUsageDescription")),
112 (
113 "EKEventStore",
114 Requirement::Key("NSCalendarsUsageDescription"),
115 ),
116 (
117 "EKReminder",
118 Requirement::Key("NSRemindersUsageDescription"),
119 ),
120 (
121 "CNContactStore",
122 Requirement::Key("NSContactsUsageDescription"),
123 ),
124 (
125 "SFSpeechRecognizer",
126 Requirement::Key("NSSpeechRecognitionUsageDescription"),
127 ),
128 (
129 "CMMotionManager",
130 Requirement::Key("NSMotionUsageDescription"),
131 ),
132 ("CMPedometer", Requirement::Key("NSMotionUsageDescription")),
133 (
134 "MPMediaLibrary",
135 Requirement::Key("NSAppleMusicUsageDescription"),
136 ),
137 (
138 "HKHealthStore",
139 Requirement::Key("NSHealthShareUsageDescription"),
140 ),
141];