verifyos_cli/core/
engine.rs1use crate::parsers::plist_reader::{InfoPlist, PlistError};
2use crate::parsers::zip_extractor::{extract_ipa, ExtractionError};
3use crate::rules::core::{
4 AppStoreRule, ArtifactCacheStats, ArtifactContext, RuleCategory, RuleError, RuleReport,
5 Severity,
6};
7use std::path::Path;
8use std::time::Instant;
9
10#[derive(Debug, thiserror::Error)]
11pub enum OrchestratorError {
12 #[error("Extraction failed: {0}")]
13 Extraction(#[from] ExtractionError),
14 #[error("Failed to parse Info.plist: {0}")]
15 PlistParse(#[from] PlistError),
16 #[error("Could not locate App Bundle (.app) inside {0}. Found entries: {1}")]
17 AppBundleNotFoundWithContext(String, String),
18 #[error("Could not locate App Bundle (.app) inside IPAPayload")]
19 AppBundleNotFound,
20}
21
22pub struct EngineResult {
23 pub rule_id: &'static str,
24 pub rule_name: &'static str,
25 pub category: RuleCategory,
26 pub severity: Severity,
27 pub target: String,
28 pub recommendation: &'static str,
29 pub report: Result<RuleReport, RuleError>,
30 pub duration_ms: u128,
31}
32
33pub struct EngineRun {
34 pub results: Vec<EngineResult>,
35 pub total_duration_ms: u128,
36 pub cache_stats: ArtifactCacheStats,
37}
38
39pub struct Engine {
40 rules: Vec<Box<dyn AppStoreRule>>,
41 pub xcode_project: Option<crate::parsers::xcode_parser::XcodeProject>,
42}
43
44impl Engine {
45 pub fn new() -> Self {
46 Self {
47 rules: Vec::new(),
48 xcode_project: None,
49 }
50 }
51
52 pub fn register_rule(&mut self, rule: Box<dyn AppStoreRule>) {
53 self.rules.push(rule);
54 }
55
56 pub fn run<P: AsRef<Path>>(&self, path_or_ipa: P) -> Result<EngineRun, OrchestratorError> {
57 let run_started = Instant::now();
58 let path = path_or_ipa.as_ref();
59
60 let mut extracted = None;
61 let targets = if path.is_dir() {
62 let mut targets = Vec::new();
64 let mut queue = vec![path.to_path_buf()];
65 while let Some(dir) = queue.pop() {
66 if let Ok(entries) = std::fs::read_dir(dir) {
67 for entry in entries.flatten() {
68 let p = entry.path();
69 if p.is_dir() {
70 let extension = p.extension().and_then(|e| e.to_str());
71 match extension {
72 Some("app") => targets.push((p.clone(), "app".to_string())),
73 Some("xcodeproj") => {
74 targets.push((p.clone(), "project".to_string()))
75 }
76 Some("xcworkspace") => {
77 targets.push((p.clone(), "workspace".to_string()))
78 }
79 _ => queue.push(p),
80 }
81 } else if p.extension().and_then(|e| e.to_str()) == Some("ipa") {
82 targets.push((p.clone(), "ipa".to_string()));
83 }
84 }
85 }
86 }
87 targets
88 } else {
89 let extracted_ipa = extract_ipa(path)?;
90 let t = extracted_ipa
91 .discover_targets()
92 .map_err(|e| OrchestratorError::Extraction(ExtractionError::Io(e)))?;
93 extracted = Some(extracted_ipa);
94 t
95 };
96
97 let mut all_results = Vec::new();
98 let mut total_cache_stats = ArtifactCacheStats::default();
99
100 if targets.is_empty() {
101 let res = if let Some(ref ext) = extracted {
103 self.run_on_bundle_internal(&ext.payload_dir, run_started, None)?
104 } else {
105 self.run_on_bundle_internal(path, run_started, None)?
106 };
107 return Ok(res);
108 }
109
110 for (target_path, target_type) in targets {
111 let project_context = if target_type == "app" {
112 let mut p_context = None;
114 if let Some(parent) = target_path.parent() {
115 if let Ok(entries) = std::fs::read_dir(parent) {
116 for entry in entries.flatten() {
117 let p = entry.path();
118 if p.extension().is_some_and(|e| e == "xcworkspace") {
119 p_context =
120 crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&p)
121 .ok()
122 .and_then(|ws| ws.project_paths.first().cloned())
123 .and_then(|proj_path| {
124 crate::parsers::xcode_parser::XcodeProject::from_path(
125 proj_path,
126 )
127 .ok()
128 });
129 break;
130 } else if p.extension().is_some_and(|e| e == "xcodeproj") {
131 p_context =
132 crate::parsers::xcode_parser::XcodeProject::from_path(&p).ok();
133 }
134 }
135 }
136 }
137 p_context
138 } else if target_type == "project" {
139 crate::parsers::xcode_parser::XcodeProject::from_path(&target_path).ok()
140 } else if target_type == "workspace" {
141 crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&target_path)
142 .ok()
143 .and_then(|ws| ws.project_paths.first().cloned())
144 .and_then(|proj_path| {
145 crate::parsers::xcode_parser::XcodeProject::from_path(proj_path).ok()
146 })
147 } else {
148 None
149 };
150
151 let app_results =
152 self.run_on_bundle_internal(&target_path, run_started, project_context)?;
153
154 let target_name = target_path
156 .file_name()
157 .map(|n| n.to_string_lossy().into_owned())
158 .unwrap_or_else(|| "Unknown".to_string());
159
160 for mut res in app_results.results {
161 res.target = target_name.clone();
162 all_results.push(res);
163 }
164
165 total_cache_stats.nested_bundles.hits += app_results.cache_stats.nested_bundles.hits;
167 total_cache_stats.nested_bundles.misses +=
168 app_results.cache_stats.nested_bundles.misses;
169 }
170
171 Ok(EngineRun {
172 results: all_results,
173 total_duration_ms: run_started.elapsed().as_millis(),
174 cache_stats: total_cache_stats,
175 })
176 }
177
178 pub fn run_on_bundle<P: AsRef<Path>>(
179 &self,
180 app_bundle_path: P,
181 run_started: Instant,
182 ) -> Result<EngineRun, OrchestratorError> {
183 self.run_on_bundle_internal(app_bundle_path, run_started, None)
184 }
185
186 fn run_on_bundle_internal<P: AsRef<Path>>(
187 &self,
188 app_bundle_path: P,
189 run_started: Instant,
190 project_override: Option<crate::parsers::xcode_parser::XcodeProject>,
191 ) -> Result<EngineRun, OrchestratorError> {
192 let app_bundle_path = app_bundle_path.as_ref();
193 let info_plist_path = app_bundle_path.join("Info.plist");
194 let info_plist = if info_plist_path.exists() {
195 Some(InfoPlist::from_file(&info_plist_path)?)
196 } else {
197 None
198 };
199
200 let context = ArtifactContext::new(
201 app_bundle_path,
202 info_plist.as_ref(),
203 project_override.as_ref().or(self.xcode_project.as_ref()),
204 );
205
206 let mut results = Vec::new();
207
208 for rule in &self.rules {
209 let rule_started = Instant::now();
210 let res = rule.evaluate(&context);
211 results.push(EngineResult {
212 rule_id: rule.id(),
213 rule_name: rule.name(),
214 category: rule.category(),
215 severity: rule.severity(),
216 target: app_bundle_path
217 .file_name()
218 .map(|n| n.to_string_lossy().into_owned())
219 .unwrap_or_else(|| "Bundle".to_string()),
220 recommendation: rule.recommendation(),
221 report: res,
222 duration_ms: rule_started.elapsed().as_millis(),
223 });
224 }
225
226 Ok(EngineRun {
227 results,
228 total_duration_ms: run_started.elapsed().as_millis(),
229 cache_stats: context.cache_stats(),
230 })
231 }
232}
233
234impl Default for Engine {
235 fn default() -> Self {
236 Self::new()
237 }
238}