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}