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::cell::RefCell;
14use std::collections::HashMap;
15use std::path::{Path, PathBuf};
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}
75
76#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
77pub enum RuleCategory {
78 Privacy,
79 Signing,
80 Bundling,
81 Entitlements,
82 Ats,
83 ThirdParty,
84 Permissions,
85 Metadata,
86 Other,
87}
88
89pub struct ArtifactContext<'a> {
91 pub app_bundle_path: &'a std::path::Path,
92 pub info_plist: Option<&'a crate::parsers::plist_reader::InfoPlist>,
93 nested_bundles_cache: RefCell<Option<Vec<BundleTarget>>>,
94 usage_scan_cache: RefCell<Option<UsageScan>>,
95 private_api_scan_cache: RefCell<Option<PrivateApiScan>>,
96 sdk_scan_cache: RefCell<Option<SdkScan>>,
97 capability_scan_cache: RefCell<Option<CapabilityScan>>,
98 signature_summary_cache: RefCell<HashMap<PathBuf, MachOSignatureSummary>>,
99 bundle_plist_cache: RefCell<HashMap<PathBuf, Option<InfoPlist>>>,
100 entitlements_cache: RefCell<HashMap<PathBuf, Option<InfoPlist>>>,
101 provisioning_profile_cache: RefCell<HashMap<PathBuf, Option<ProvisioningProfile>>>,
102 bundle_file_cache: RefCell<Option<Vec<PathBuf>>>,
103 cache_stats: RefCell<ArtifactCacheStats>,
104}
105
106impl<'a> ArtifactContext<'a> {
107 pub fn new(
108 app_bundle_path: &'a Path,
109 info_plist: Option<&'a crate::parsers::plist_reader::InfoPlist>,
110 ) -> Self {
111 Self {
112 app_bundle_path,
113 info_plist,
114 nested_bundles_cache: RefCell::new(None),
115 usage_scan_cache: RefCell::new(None),
116 private_api_scan_cache: RefCell::new(None),
117 sdk_scan_cache: RefCell::new(None),
118 capability_scan_cache: RefCell::new(None),
119 signature_summary_cache: RefCell::new(HashMap::new()),
120 bundle_plist_cache: RefCell::new(HashMap::new()),
121 entitlements_cache: RefCell::new(HashMap::new()),
122 provisioning_profile_cache: RefCell::new(HashMap::new()),
123 bundle_file_cache: RefCell::new(None),
124 cache_stats: RefCell::new(ArtifactCacheStats::default()),
125 }
126 }
127
128 pub fn nested_bundles(&self) -> Result<Vec<BundleTarget>, BundleScanError> {
129 if let Some(bundles) = self.nested_bundles_cache.borrow().as_ref() {
130 self.cache_stats.borrow_mut().nested_bundles.hits += 1;
131 return Ok(bundles.clone());
132 }
133
134 self.cache_stats.borrow_mut().nested_bundles.misses += 1;
135 let bundles = find_nested_bundles(self.app_bundle_path)?;
136 *self.nested_bundles_cache.borrow_mut() = Some(bundles.clone());
137 Ok(bundles)
138 }
139
140 pub fn usage_scan(&self) -> Result<UsageScan, UsageScanError> {
141 if let Some(scan) = self.usage_scan_cache.borrow().as_ref() {
142 self.cache_stats.borrow_mut().usage_scan.hits += 1;
143 return Ok(scan.clone());
144 }
145
146 self.cache_stats.borrow_mut().usage_scan.misses += 1;
147 let scan = scan_usage_from_app_bundle(self.app_bundle_path)?;
148 *self.usage_scan_cache.borrow_mut() = Some(scan.clone());
149 Ok(scan)
150 }
151
152 pub fn private_api_scan(&self) -> Result<PrivateApiScan, UsageScanError> {
153 if let Some(scan) = self.private_api_scan_cache.borrow().as_ref() {
154 self.cache_stats.borrow_mut().private_api_scan.hits += 1;
155 return Ok(scan.clone());
156 }
157
158 self.cache_stats.borrow_mut().private_api_scan.misses += 1;
159 let scan = scan_private_api_from_app_bundle(self.app_bundle_path)?;
160 *self.private_api_scan_cache.borrow_mut() = Some(scan.clone());
161 Ok(scan)
162 }
163
164 pub fn sdk_scan(&self) -> Result<SdkScan, UsageScanError> {
165 if let Some(scan) = self.sdk_scan_cache.borrow().as_ref() {
166 self.cache_stats.borrow_mut().sdk_scan.hits += 1;
167 return Ok(scan.clone());
168 }
169
170 self.cache_stats.borrow_mut().sdk_scan.misses += 1;
171 let scan = scan_sdks_from_app_bundle(self.app_bundle_path)?;
172 *self.sdk_scan_cache.borrow_mut() = Some(scan.clone());
173 Ok(scan)
174 }
175
176 pub fn capability_scan(&self) -> Result<CapabilityScan, UsageScanError> {
177 if let Some(scan) = self.capability_scan_cache.borrow().as_ref() {
178 self.cache_stats.borrow_mut().capability_scan.hits += 1;
179 return Ok(scan.clone());
180 }
181
182 self.cache_stats.borrow_mut().capability_scan.misses += 1;
183 let scan = scan_capabilities_from_app_bundle(self.app_bundle_path)?;
184 *self.capability_scan_cache.borrow_mut() = Some(scan.clone());
185 Ok(scan)
186 }
187
188 pub fn signature_summary(
189 &self,
190 executable_path: impl AsRef<Path>,
191 ) -> Result<MachOSignatureSummary, MachOError> {
192 let executable_path = executable_path.as_ref().to_path_buf();
193 if let Some(summary) = self.signature_summary_cache.borrow().get(&executable_path) {
194 self.cache_stats.borrow_mut().signature_summary.hits += 1;
195 return Ok(summary.clone());
196 }
197
198 self.cache_stats.borrow_mut().signature_summary.misses += 1;
199 let summary = read_macho_signature_summary(&executable_path)?;
200 self.signature_summary_cache
201 .borrow_mut()
202 .insert(executable_path, summary.clone());
203 Ok(summary)
204 }
205
206 pub fn executable_path_for_bundle(&self, bundle_path: &Path) -> Option<PathBuf> {
207 if let Ok(Some(plist)) = self.bundle_info_plist(bundle_path) {
208 if let Some(executable) = plist.get_string("CFBundleExecutable") {
209 let candidate = bundle_path.join(executable);
210 if candidate.exists() {
211 return Some(candidate);
212 }
213 }
214 }
215
216 resolve_bundle_executable_path(bundle_path)
217 }
218
219 pub fn bundle_info_plist(&self, bundle_path: &Path) -> Result<Option<InfoPlist>, PlistError> {
220 if let Some(plist) = self.bundle_plist_cache.borrow().get(bundle_path) {
221 self.cache_stats.borrow_mut().bundle_plist.hits += 1;
222 return Ok(plist.clone());
223 }
224
225 self.cache_stats.borrow_mut().bundle_plist.misses += 1;
226 let plist_path = bundle_path.join("Info.plist");
227 let plist = if plist_path.exists() {
228 Some(InfoPlist::from_file(&plist_path)?)
229 } else {
230 None
231 };
232
233 self.bundle_plist_cache
234 .borrow_mut()
235 .insert(bundle_path.to_path_buf(), plist.clone());
236 Ok(plist)
237 }
238
239 pub fn entitlements_for_bundle(
240 &self,
241 bundle_path: &Path,
242 ) -> Result<Option<InfoPlist>, RuleError> {
243 let executable_path = match self.executable_path_for_bundle(bundle_path) {
244 Some(path) => path,
245 None => return Ok(None),
246 };
247
248 if let Some(entitlements) = self.entitlements_cache.borrow().get(&executable_path) {
249 self.cache_stats.borrow_mut().entitlements.hits += 1;
250 return Ok(entitlements.clone());
251 }
252
253 self.cache_stats.borrow_mut().entitlements.misses += 1;
254 let macho = MachOExecutable::from_file(&executable_path)
255 .map_err(crate::rules::entitlements::EntitlementsError::MachO)
256 .map_err(RuleError::Entitlements)?;
257 let entitlements = match macho.entitlements {
258 Some(entitlements_xml) => {
259 let plist = InfoPlist::from_bytes(entitlements_xml.as_bytes())
260 .map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
261 Some(plist)
262 }
263 None => None,
264 };
265
266 self.entitlements_cache
267 .borrow_mut()
268 .insert(executable_path, entitlements.clone());
269 Ok(entitlements)
270 }
271
272 pub fn provisioning_profile_for_bundle(
273 &self,
274 bundle_path: &Path,
275 ) -> Result<Option<ProvisioningProfile>, ProvisioningError> {
276 let provisioning_path = bundle_path.join("embedded.mobileprovision");
277 if let Some(profile) = self
278 .provisioning_profile_cache
279 .borrow()
280 .get(&provisioning_path)
281 {
282 self.cache_stats.borrow_mut().provisioning_profile.hits += 1;
283 return Ok(profile.clone());
284 }
285
286 self.cache_stats.borrow_mut().provisioning_profile.misses += 1;
287 let profile = if provisioning_path.exists() {
288 Some(ProvisioningProfile::from_embedded_file(&provisioning_path)?)
289 } else {
290 None
291 };
292
293 self.provisioning_profile_cache
294 .borrow_mut()
295 .insert(provisioning_path, profile.clone());
296 Ok(profile)
297 }
298
299 pub fn bundle_file_paths(&self) -> Vec<PathBuf> {
300 if let Some(paths) = self.bundle_file_cache.borrow().as_ref() {
301 self.cache_stats.borrow_mut().bundle_files.hits += 1;
302 return paths.clone();
303 }
304
305 self.cache_stats.borrow_mut().bundle_files.misses += 1;
306 let mut files = Vec::new();
307 collect_bundle_files(self.app_bundle_path, &mut files);
308 *self.bundle_file_cache.borrow_mut() = Some(files.clone());
309 files
310 }
311
312 pub fn bundle_relative_file(&self, relative_path: &str) -> Option<PathBuf> {
313 self.bundle_file_paths().into_iter().find(|path| {
314 path.strip_prefix(self.app_bundle_path)
315 .ok()
316 .map(|rel| rel == Path::new(relative_path))
317 .unwrap_or(false)
318 })
319 }
320
321 pub fn cache_stats(&self) -> ArtifactCacheStats {
322 self.cache_stats.borrow().clone()
323 }
324}
325
326fn resolve_bundle_executable_path(bundle_path: &Path) -> Option<PathBuf> {
327 let bundle_name = bundle_path
328 .file_name()
329 .and_then(|n| n.to_str())
330 .unwrap_or("")
331 .trim_end_matches(".app")
332 .trim_end_matches(".appex")
333 .trim_end_matches(".framework");
334
335 if bundle_name.is_empty() {
336 return None;
337 }
338
339 let fallback = bundle_path.join(bundle_name);
340 if fallback.exists() {
341 Some(fallback)
342 } else {
343 None
344 }
345}
346
347fn collect_bundle_files(root: &Path, files: &mut Vec<PathBuf>) {
348 let entries = match std::fs::read_dir(root) {
349 Ok(entries) => entries,
350 Err(_) => return,
351 };
352
353 for entry in entries.flatten() {
354 let path = entry.path();
355 if path.is_dir() {
356 collect_bundle_files(&path, files);
357 } else {
358 files.push(path);
359 }
360 }
361}
362
363pub trait AppStoreRule: Send + Sync {
364 fn id(&self) -> &'static str;
365 fn name(&self) -> &'static str;
366 fn category(&self) -> RuleCategory;
367 fn severity(&self) -> Severity;
368 fn recommendation(&self) -> &'static str;
369 fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError>;
370}