1use crate::parsers::bundle_scanner::{find_nested_bundles, BundleScanError, BundleTarget};
2use crate::parsers::macho_parser::{
3 read_macho_signature_summary, MachOError, MachOExecutable, MachOSignatureSummary,
4};
5use crate::parsers::macho_scanner::{
6 scan_capabilities_from_app_bundle, scan_private_api_from_app_bundle, scan_sdks_from_app_bundle,
7 scan_usage_from_app_bundle, CapabilityScan, PrivateApiScan, SdkScan, UsageScan, UsageScanError,
8};
9use crate::parsers::plist_reader::{InfoPlist, PlistError};
10use crate::parsers::provisioning_profile::{ProvisioningError, ProvisioningProfile};
11use miette::Diagnostic;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15use std::sync::Mutex;
16
17pub const RULESET_VERSION: &str = "0.1.0";
18
19#[derive(Debug, thiserror::Error, Diagnostic)]
20pub enum RuleError {
21 #[error(transparent)]
22 #[diagnostic(transparent)]
23 Entitlements(#[from] crate::rules::entitlements::EntitlementsError),
24
25 #[error(transparent)]
26 #[diagnostic(transparent)]
27 Provisioning(#[from] crate::parsers::provisioning_profile::ProvisioningError),
28
29 #[error(transparent)]
30 #[diagnostic(transparent)]
31 MachO(#[from] crate::parsers::macho_parser::MachOError),
32}
33
34#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
35pub enum Severity {
36 Error,
37 Warning,
38 Info,
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
42pub enum RuleStatus {
43 Pass,
44 Fail,
45 Error,
46 Skip,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct RuleReport {
51 pub status: RuleStatus,
52 pub message: Option<String>,
53 pub evidence: Option<String>,
54}
55
56#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
57pub struct CacheCounter {
58 pub hits: u64,
59 pub misses: u64,
60}
61
62#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
63pub struct ArtifactCacheStats {
64 pub nested_bundles: CacheCounter,
65 pub usage_scan: CacheCounter,
66 pub private_api_scan: CacheCounter,
67 pub sdk_scan: CacheCounter,
68 pub capability_scan: CacheCounter,
69 pub signature_summary: CacheCounter,
70 pub bundle_plist: CacheCounter,
71 pub entitlements: CacheCounter,
72 pub provisioning_profile: CacheCounter,
73 pub bundle_files: CacheCounter,
74 pub instrumentation_scan: CacheCounter,
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
78pub enum RuleCategory {
79 Privacy,
80 Signing,
81 Bundling,
82 Entitlements,
83 Ats,
84 ThirdParty,
85 Permissions,
86 Metadata,
87 Other,
88}
89
90pub struct ArtifactContext<'a> {
92 pub app_bundle_path: &'a std::path::Path,
93 pub info_plist: Option<&'a crate::parsers::plist_reader::InfoPlist>,
94 nested_bundles_cache: Mutex<Option<Vec<BundleTarget>>>,
95 usage_scan_cache: Mutex<Option<UsageScan>>,
96 private_api_scan_cache: Mutex<Option<PrivateApiScan>>,
97 sdk_scan_cache: Mutex<Option<SdkScan>>,
98 capability_scan_cache: Mutex<Option<CapabilityScan>>,
99 signature_summary_cache: Mutex<HashMap<PathBuf, MachOSignatureSummary>>,
100 bundle_plist_cache: Mutex<HashMap<PathBuf, Option<InfoPlist>>>,
101 entitlements_cache: Mutex<HashMap<PathBuf, Option<InfoPlist>>>,
102 provisioning_profile_cache: Mutex<HashMap<PathBuf, Option<ProvisioningProfile>>>,
103 bundle_file_cache: Mutex<Option<Vec<PathBuf>>>,
104 instrumentation_scan_cache: Mutex<Option<Vec<&'static str>>>,
105 pub xcode_project: Option<&'a crate::parsers::xcode_parser::XcodeProject>,
106 cache_stats: Mutex<ArtifactCacheStats>,
107}
108
109impl<'a> ArtifactContext<'a> {
110 pub fn new(
111 app_bundle_path: &'a Path,
112 info_plist: Option<&'a crate::parsers::plist_reader::InfoPlist>,
113 xcode_project: Option<&'a crate::parsers::xcode_parser::XcodeProject>,
114 ) -> Self {
115 Self {
116 app_bundle_path,
117 info_plist,
118 nested_bundles_cache: Mutex::new(None),
119 usage_scan_cache: Mutex::new(None),
120 private_api_scan_cache: Mutex::new(None),
121 sdk_scan_cache: Mutex::new(None),
122 capability_scan_cache: Mutex::new(None),
123 signature_summary_cache: Mutex::new(HashMap::new()),
124 bundle_plist_cache: Mutex::new(HashMap::new()),
125 entitlements_cache: Mutex::new(HashMap::new()),
126 provisioning_profile_cache: Mutex::new(HashMap::new()),
127 bundle_file_cache: Mutex::new(None),
128 instrumentation_scan_cache: Mutex::new(None),
129 xcode_project,
130 cache_stats: Mutex::new(ArtifactCacheStats::default()),
131 }
132 }
133
134 pub fn nested_bundles(&self) -> Result<Vec<BundleTarget>, BundleScanError> {
135 if let Some(bundles) = self.nested_bundles_cache.lock().unwrap().as_ref() {
136 self.cache_stats.lock().unwrap().nested_bundles.hits += 1;
137 return Ok(bundles.clone());
138 }
139
140 self.cache_stats.lock().unwrap().nested_bundles.misses += 1;
141 let bundles = find_nested_bundles(self.app_bundle_path)?;
142 *self.nested_bundles_cache.lock().unwrap() = Some(bundles.clone());
143 Ok(bundles)
144 }
145
146 pub fn usage_scan(&self) -> Result<UsageScan, UsageScanError> {
147 if let Some(scan) = self.usage_scan_cache.lock().unwrap().as_ref() {
148 self.cache_stats.lock().unwrap().usage_scan.hits += 1;
149 return Ok(scan.clone());
150 }
151
152 self.cache_stats.lock().unwrap().usage_scan.misses += 1;
153 let scan = scan_usage_from_app_bundle(self.app_bundle_path)?;
154 *self.usage_scan_cache.lock().unwrap() = Some(scan.clone());
155 Ok(scan)
156 }
157
158 pub fn private_api_scan(&self) -> Result<PrivateApiScan, UsageScanError> {
159 if let Some(scan) = self.private_api_scan_cache.lock().unwrap().as_ref() {
160 self.cache_stats.lock().unwrap().private_api_scan.hits += 1;
161 return Ok(scan.clone());
162 }
163
164 self.cache_stats.lock().unwrap().private_api_scan.misses += 1;
165 let scan = scan_private_api_from_app_bundle(self.app_bundle_path)?;
166 *self.private_api_scan_cache.lock().unwrap() = Some(scan.clone());
167 Ok(scan)
168 }
169
170 pub fn sdk_scan(&self) -> Result<SdkScan, UsageScanError> {
171 if let Some(scan) = self.sdk_scan_cache.lock().unwrap().as_ref() {
172 self.cache_stats.lock().unwrap().sdk_scan.hits += 1;
173 return Ok(scan.clone());
174 }
175
176 self.cache_stats.lock().unwrap().sdk_scan.misses += 1;
177 let scan = scan_sdks_from_app_bundle(self.app_bundle_path)?;
178 *self.sdk_scan_cache.lock().unwrap() = Some(scan.clone());
179 Ok(scan)
180 }
181
182 pub fn capability_scan(&self) -> Result<CapabilityScan, UsageScanError> {
183 if let Some(scan) = self.capability_scan_cache.lock().unwrap().as_ref() {
184 self.cache_stats.lock().unwrap().capability_scan.hits += 1;
185 return Ok(scan.clone());
186 }
187
188 self.cache_stats.lock().unwrap().capability_scan.misses += 1;
189 let scan = scan_capabilities_from_app_bundle(self.app_bundle_path)?;
190 *self.capability_scan_cache.lock().unwrap() = Some(scan.clone());
191 Ok(scan)
192 }
193
194 pub fn instrumentation_scan(&self) -> Result<Vec<&'static str>, UsageScanError> {
195 if let Some(hits) = self.instrumentation_scan_cache.lock().unwrap().as_ref() {
196 self.cache_stats.lock().unwrap().instrumentation_scan.hits += 1;
197 return Ok(hits.clone());
198 }
199
200 self.cache_stats.lock().unwrap().instrumentation_scan.misses += 1;
201 let hits = crate::parsers::macho_scanner::scan_instrumentation_from_app_bundle(
202 self.app_bundle_path,
203 )?;
204 *self.instrumentation_scan_cache.lock().unwrap() = Some(hits.clone());
205 Ok(hits)
206 }
207
208 pub fn signature_summary(
209 &self,
210 executable_path: impl AsRef<Path>,
211 ) -> Result<MachOSignatureSummary, MachOError> {
212 let executable_path = executable_path.as_ref().to_path_buf();
213 if let Some(summary) = self
214 .signature_summary_cache
215 .lock()
216 .unwrap()
217 .get(&executable_path)
218 {
219 self.cache_stats.lock().unwrap().signature_summary.hits += 1;
220 return Ok(summary.clone());
221 }
222
223 self.cache_stats.lock().unwrap().signature_summary.misses += 1;
224 let summary = read_macho_signature_summary(&executable_path)?;
225 self.signature_summary_cache
226 .lock()
227 .unwrap()
228 .insert(executable_path, summary.clone());
229 Ok(summary)
230 }
231
232 pub fn executable_path_for_bundle(&self, bundle_path: &Path) -> Option<PathBuf> {
233 if let Ok(Some(plist)) = self.bundle_info_plist(bundle_path) {
234 if let Some(executable) = plist.get_string("CFBundleExecutable") {
235 let candidate = bundle_path.join(executable);
236 if candidate.exists() {
237 return Some(candidate);
238 }
239 }
240 }
241
242 resolve_bundle_executable_path(bundle_path)
243 }
244
245 pub fn bundle_info_plist(&self, bundle_path: &Path) -> Result<Option<InfoPlist>, PlistError> {
246 if let Some(plist) = self.bundle_plist_cache.lock().unwrap().get(bundle_path) {
247 self.cache_stats.lock().unwrap().bundle_plist.hits += 1;
248 return Ok(plist.clone());
249 }
250
251 self.cache_stats.lock().unwrap().bundle_plist.misses += 1;
252 let plist_path = bundle_path.join("Info.plist");
253 let plist = if plist_path.exists() {
254 Some(InfoPlist::from_file(&plist_path)?)
255 } else {
256 None
257 };
258
259 self.bundle_plist_cache
260 .lock()
261 .unwrap()
262 .insert(bundle_path.to_path_buf(), plist.clone());
263 Ok(plist)
264 }
265
266 pub fn entitlements_for_bundle(
267 &self,
268 bundle_path: &Path,
269 ) -> Result<Option<InfoPlist>, RuleError> {
270 let executable_path = match self.executable_path_for_bundle(bundle_path) {
271 Some(path) => path,
272 None => return Ok(None),
273 };
274
275 if let Some(entitlements) = self
276 .entitlements_cache
277 .lock()
278 .unwrap()
279 .get(&executable_path)
280 {
281 self.cache_stats.lock().unwrap().entitlements.hits += 1;
282 return Ok(entitlements.clone());
283 }
284
285 self.cache_stats.lock().unwrap().entitlements.misses += 1;
286 let macho = MachOExecutable::from_file(&executable_path)
287 .map_err(crate::rules::entitlements::EntitlementsError::MachO)
288 .map_err(RuleError::Entitlements)?;
289 let entitlements = match macho.entitlements {
290 Some(entitlements_xml) => {
291 let plist = InfoPlist::from_bytes(entitlements_xml.as_bytes())
292 .map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
293 Some(plist)
294 }
295 None => None,
296 };
297
298 self.entitlements_cache
299 .lock()
300 .unwrap()
301 .insert(executable_path, entitlements.clone());
302 Ok(entitlements)
303 }
304
305 pub fn provisioning_profile_for_bundle(
306 &self,
307 bundle_path: &Path,
308 ) -> Result<Option<ProvisioningProfile>, ProvisioningError> {
309 let provisioning_path = bundle_path.join("embedded.mobileprovision");
310 if let Some(profile) = self
311 .provisioning_profile_cache
312 .lock()
313 .unwrap()
314 .get(&provisioning_path)
315 {
316 self.cache_stats.lock().unwrap().provisioning_profile.hits += 1;
317 return Ok(profile.clone());
318 }
319
320 self.cache_stats.lock().unwrap().provisioning_profile.misses += 1;
321 let profile = if provisioning_path.exists() {
322 Some(ProvisioningProfile::from_embedded_file(&provisioning_path)?)
323 } else {
324 None
325 };
326
327 self.provisioning_profile_cache
328 .lock()
329 .unwrap()
330 .insert(provisioning_path, profile.clone());
331 Ok(profile)
332 }
333
334 pub fn bundle_file_paths(&self) -> Vec<PathBuf> {
335 if let Some(paths) = self.bundle_file_cache.lock().unwrap().as_ref() {
336 self.cache_stats.lock().unwrap().bundle_files.hits += 1;
337 return paths.clone();
338 }
339
340 self.cache_stats.lock().unwrap().bundle_files.misses += 1;
341 let mut files = Vec::new();
342 collect_bundle_files(self.app_bundle_path, &mut files);
343 *self.bundle_file_cache.lock().unwrap() = Some(files.clone());
344 files
345 }
346
347 pub fn bundle_relative_file(&self, relative_path: &str) -> Option<PathBuf> {
348 self.bundle_file_paths().into_iter().find(|path| {
349 path.strip_prefix(self.app_bundle_path)
350 .ok()
351 .map(|rel| rel == Path::new(relative_path))
352 .unwrap_or(false)
353 })
354 }
355
356 pub fn cache_stats(&self) -> ArtifactCacheStats {
357 self.cache_stats.lock().unwrap().clone()
358 }
359}
360
361fn resolve_bundle_executable_path(bundle_path: &Path) -> Option<PathBuf> {
362 let bundle_name = bundle_path
363 .file_name()
364 .and_then(|n| n.to_str())
365 .unwrap_or("")
366 .trim_end_matches(".app")
367 .trim_end_matches(".appex")
368 .trim_end_matches(".framework");
369
370 if bundle_name.is_empty() {
371 return None;
372 }
373
374 let fallback = bundle_path.join(bundle_name);
375 if fallback.exists() {
376 Some(fallback)
377 } else {
378 None
379 }
380}
381
382fn collect_bundle_files(root: &Path, files: &mut Vec<PathBuf>) {
383 let entries = match std::fs::read_dir(root) {
384 Ok(entries) => entries,
385 Err(_) => return,
386 };
387
388 for entry in entries.flatten() {
389 let path = entry.path();
390 if path.is_dir() {
391 collect_bundle_files(&path, files);
392 } else {
393 files.push(path);
394 }
395 }
396}
397
398pub trait AppStoreRule: Send + Sync {
399 fn id(&self) -> &'static str;
400 fn name(&self) -> &'static str;
401 fn category(&self) -> RuleCategory;
402 fn severity(&self) -> Severity;
403 fn recommendation(&self) -> &'static str;
404 fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError>;
405}