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, ipa_path: P) -> Result<EngineRun, OrchestratorError> {
57 let run_started = Instant::now();
58 let path = ipa_path.as_ref();
59
60 if path.is_dir() {
61 return self.run_on_bundle(path, run_started);
62 }
63
64 let extracted_ipa = extract_ipa(path)?;
65 let targets = extracted_ipa
66 .discover_targets()
67 .map_err(|e| OrchestratorError::Extraction(ExtractionError::Io(e)))?;
68
69 let mut all_results = Vec::new();
70 let mut total_cache_stats = ArtifactCacheStats::default();
71
72 if targets.is_empty() {
73 let res = self.run_on_bundle_internal(&extracted_ipa.payload_dir, run_started, None)?;
75 return Ok(res);
76 }
77
78 for (target_path, target_type) in targets {
79 let project_context = if target_type == "app" {
80 extracted_ipa.get_project_path().ok().flatten().and_then(|p| {
82 if p.extension().is_some_and(|e| e == "xcworkspace") {
83 crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&p)
84 .ok()
85 .and_then(|ws| ws.project_paths.first().cloned())
86 .and_then(|proj_path| {
87 crate::parsers::xcode_parser::XcodeProject::from_path(proj_path).ok()
88 })
89 } else {
90 crate::parsers::xcode_parser::XcodeProject::from_path(&p).ok()
91 }
92 })
93 } else if target_type == "project" {
94 crate::parsers::xcode_parser::XcodeProject::from_path(&target_path).ok()
95 } else if target_type == "workspace" {
96 crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&target_path)
97 .ok()
98 .and_then(|ws| ws.project_paths.first().cloned())
99 .and_then(|proj_path| {
100 crate::parsers::xcode_parser::XcodeProject::from_path(proj_path).ok()
101 })
102 } else {
103 None
104 };
105
106 let app_results =
107 self.run_on_bundle_internal(&target_path, run_started, project_context)?;
108
109 let target_name = target_path
111 .file_name()
112 .map(|n| n.to_string_lossy().into_owned())
113 .unwrap_or_else(|| "Unknown".to_string());
114
115 for mut res in app_results.results {
116 res.target = target_name.clone();
117 all_results.push(res);
118 }
119
120 total_cache_stats.nested_bundles.hits += app_results.cache_stats.nested_bundles.hits;
122 total_cache_stats.nested_bundles.misses += app_results.cache_stats.nested_bundles.misses;
123 }
125
126 Ok(EngineRun {
127 results: all_results,
128 total_duration_ms: run_started.elapsed().as_millis(),
129 cache_stats: total_cache_stats,
130 })
131 }
132
133 pub fn run_on_bundle<P: AsRef<Path>>(
134 &self,
135 app_bundle_path: P,
136 run_started: Instant,
137 ) -> Result<EngineRun, OrchestratorError> {
138 self.run_on_bundle_internal(app_bundle_path, run_started, None)
139 }
140
141 fn run_on_bundle_internal<P: AsRef<Path>>(
142 &self,
143 app_bundle_path: P,
144 run_started: Instant,
145 project_override: Option<crate::parsers::xcode_parser::XcodeProject>,
146 ) -> Result<EngineRun, OrchestratorError> {
147 let app_bundle_path = app_bundle_path.as_ref();
148 let info_plist_path = app_bundle_path.join("Info.plist");
149 let info_plist = if info_plist_path.exists() {
150 Some(InfoPlist::from_file(&info_plist_path)?)
151 } else {
152 None
153 };
154
155 let context = ArtifactContext::new(
156 app_bundle_path,
157 info_plist.as_ref(),
158 project_override.as_ref().or(self.xcode_project.as_ref()),
159 );
160
161 let mut results = Vec::new();
162
163 for rule in &self.rules {
164 let rule_started = Instant::now();
165 let res = rule.evaluate(&context);
166 results.push(EngineResult {
167 rule_id: rule.id(),
168 rule_name: rule.name(),
169 category: rule.category(),
170 severity: rule.severity(),
171 target: app_bundle_path
172 .file_name()
173 .map(|n| n.to_string_lossy().into_owned())
174 .unwrap_or_else(|| "Bundle".to_string()),
175 recommendation: rule.recommendation(),
176 report: res,
177 duration_ms: rule_started.elapsed().as_millis(),
178 });
179 }
180
181 Ok(EngineRun {
182 results,
183 total_duration_ms: run_started.elapsed().as_millis(),
184 cache_stats: context.cache_stats(),
185 })
186 }
187}
188
189impl Default for Engine {
190 fn default() -> Self {
191 Self::new()
192 }
193}