Skip to main content

verifyos_cli/rules/
info_plist.rs

1use crate::parsers::plist_reader::InfoPlist;
2use crate::rules::core::{
3    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
4};
5
6const USAGE_DESCRIPTION_KEYS: &[&str] = &[
7    "NSCameraUsageDescription",
8    "NSMicrophoneUsageDescription",
9    "NSPhotoLibraryUsageDescription",
10    "NSPhotoLibraryAddUsageDescription",
11    "NSLocationWhenInUseUsageDescription",
12    "NSLocationAlwaysAndWhenInUseUsageDescription",
13    "NSLocationAlwaysUsageDescription",
14    "NSBluetoothAlwaysUsageDescription",
15    "NSBluetoothPeripheralUsageDescription",
16    "NSFaceIDUsageDescription",
17    "NSCalendarsUsageDescription",
18    "NSRemindersUsageDescription",
19    "NSContactsUsageDescription",
20    "NSSpeechRecognitionUsageDescription",
21    "NSMotionUsageDescription",
22    "NSAppleMusicUsageDescription",
23    "NSHealthShareUsageDescription",
24    "NSHealthUpdateUsageDescription",
25];
26
27pub struct UsageDescriptionsRule;
28
29impl AppStoreRule for UsageDescriptionsRule {
30    fn id(&self) -> &'static str {
31        "RULE_USAGE_DESCRIPTIONS"
32    }
33
34    fn name(&self) -> &'static str {
35        "Missing Usage Description Keys"
36    }
37
38    fn category(&self) -> RuleCategory {
39        RuleCategory::Privacy
40    }
41
42    fn severity(&self) -> Severity {
43        Severity::Warning
44    }
45
46    fn recommendation(&self) -> &'static str {
47        "Add NS*UsageDescription keys required by your app's feature usage."
48    }
49
50    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
51        let Some(plist) = artifact.info_plist else {
52            return Ok(RuleReport {
53                status: RuleStatus::Skip,
54                message: Some("Info.plist not found".to_string()),
55                evidence: None,
56            });
57        };
58
59        let missing: Vec<&str> = USAGE_DESCRIPTION_KEYS
60            .iter()
61            .copied()
62            .filter(|key| !plist.has_key(key))
63            .collect();
64
65        if missing.is_empty() {
66            return Ok(RuleReport {
67                status: RuleStatus::Pass,
68                message: None,
69                evidence: None,
70            });
71        }
72
73        Ok(RuleReport {
74            status: RuleStatus::Fail,
75            message: Some("One or more NS*UsageDescription keys are missing".to_string()),
76            evidence: Some(format!("Missing keys: {}", missing.join(", "))),
77        })
78    }
79}
80
81pub struct UsageDescriptionsValueRule;
82
83impl AppStoreRule for UsageDescriptionsValueRule {
84    fn id(&self) -> &'static str {
85        "RULE_USAGE_DESCRIPTIONS_EMPTY"
86    }
87
88    fn name(&self) -> &'static str {
89        "Empty Usage Description Values"
90    }
91
92    fn category(&self) -> RuleCategory {
93        RuleCategory::Privacy
94    }
95
96    fn severity(&self) -> Severity {
97        Severity::Warning
98    }
99
100    fn recommendation(&self) -> &'static str {
101        "Ensure NS*UsageDescription values are non-empty and user-facing."
102    }
103
104    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
105        let Some(plist) = artifact.info_plist else {
106            return Ok(RuleReport {
107                status: RuleStatus::Skip,
108                message: Some("Info.plist not found".to_string()),
109                evidence: None,
110            });
111        };
112
113        let empty: Vec<&str> = USAGE_DESCRIPTION_KEYS
114            .iter()
115            .copied()
116            .filter(|key| is_empty_string(plist, key))
117            .collect();
118
119        if empty.is_empty() {
120            return Ok(RuleReport {
121                status: RuleStatus::Pass,
122                message: None,
123                evidence: None,
124            });
125        }
126
127        Ok(RuleReport {
128            status: RuleStatus::Fail,
129            message: Some("Usage description values are empty".to_string()),
130            evidence: Some(format!("Empty keys: {}", empty.join(", "))),
131        })
132    }
133}
134
135pub struct InfoPlistRequiredKeysRule;
136
137impl AppStoreRule for InfoPlistRequiredKeysRule {
138    fn id(&self) -> &'static str {
139        "RULE_INFO_PLIST_REQUIRED_KEYS"
140    }
141
142    fn name(&self) -> &'static str {
143        "Missing Required Info.plist Keys"
144    }
145
146    fn category(&self) -> RuleCategory {
147        RuleCategory::Metadata
148    }
149
150    fn severity(&self) -> Severity {
151        Severity::Warning
152    }
153
154    fn recommendation(&self) -> &'static str {
155        "Add required Info.plist keys for your app's functionality."
156    }
157
158    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
159        let Some(plist) = artifact.info_plist else {
160            return Ok(RuleReport {
161                status: RuleStatus::Skip,
162                message: Some("Info.plist not found".to_string()),
163                evidence: None,
164            });
165        };
166
167        let mut missing = Vec::new();
168        if !plist.has_key("LSApplicationQueriesSchemes") {
169            missing.push("LSApplicationQueriesSchemes");
170        }
171        if !plist.has_key("UIRequiredDeviceCapabilities") {
172            missing.push("UIRequiredDeviceCapabilities");
173        }
174
175        if missing.is_empty() {
176            return Ok(RuleReport {
177                status: RuleStatus::Pass,
178                message: None,
179                evidence: None,
180            });
181        }
182
183        Ok(RuleReport {
184            status: RuleStatus::Fail,
185            message: Some("Missing required Info.plist keys".to_string()),
186            evidence: Some(format!("Missing keys: {}", missing.join(", "))),
187        })
188    }
189}
190
191pub struct InfoPlistCapabilitiesRule;
192
193impl AppStoreRule for InfoPlistCapabilitiesRule {
194    fn id(&self) -> &'static str {
195        "RULE_INFO_PLIST_CAPABILITIES_EMPTY"
196    }
197
198    fn name(&self) -> &'static str {
199        "Empty Info.plist Capability Lists"
200    }
201
202    fn category(&self) -> RuleCategory {
203        RuleCategory::Metadata
204    }
205
206    fn severity(&self) -> Severity {
207        Severity::Warning
208    }
209
210    fn recommendation(&self) -> &'static str {
211        "Remove empty arrays or populate capability keys with valid values."
212    }
213
214    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
215        let Some(plist) = artifact.info_plist else {
216            return Ok(RuleReport {
217                status: RuleStatus::Skip,
218                message: Some("Info.plist not found".to_string()),
219                evidence: None,
220            });
221        };
222
223        let mut empty = Vec::new();
224
225        if is_empty_array(plist, "LSApplicationQueriesSchemes") {
226            empty.push("LSApplicationQueriesSchemes");
227        }
228
229        if is_empty_array(plist, "UIRequiredDeviceCapabilities") {
230            empty.push("UIRequiredDeviceCapabilities");
231        }
232
233        if empty.is_empty() {
234            return Ok(RuleReport {
235                status: RuleStatus::Pass,
236                message: None,
237                evidence: None,
238            });
239        }
240
241        Ok(RuleReport {
242            status: RuleStatus::Fail,
243            message: Some("Capability keys are present but empty".to_string()),
244            evidence: Some(format!("Empty keys: {}", empty.join(", "))),
245        })
246    }
247}
248
249fn is_empty_string(plist: &InfoPlist, key: &str) -> bool {
250    match plist.get_string(key) {
251        Some(value) => value.trim().is_empty(),
252        None => false,
253    }
254}
255
256fn is_empty_array(plist: &InfoPlist, key: &str) -> bool {
257    match plist.get_value(key) {
258        Some(value) => value.as_array().map(|arr| arr.is_empty()).unwrap_or(false),
259        None => false,
260    }
261}