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