Skip to content

Commit 9fe3854

Browse files
committed
feat(engine): implement multi-target scanning and result aggregation
1 parent b437245 commit 9fe3854

13 files changed

Lines changed: 141 additions & 74 deletions

File tree

examples/Payload/BadApp.app/BadApp

82.2 KB
Binary file not shown.

examples/Payload/BadApp.app/Info.plist

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,9 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<!-- Missing NSCameraUsageDescription intentionally to trigger rule failure -->
6-
<key>CFBundleIdentifier</key>
7-
<string>com.example.BadApp</string>
8-
<key>LSApplicationQueriesSchemes</key>
9-
<array>
10-
<string>fb</string>
11-
</array>
12-
<key>UIRequiredDeviceCapabilities</key>
13-
<array>
14-
<string>arm64</string>
15-
</array>
16-
<key>CFBundleShortVersionString</key>
17-
<string>1.0.0</string>
18-
<key>CFBundleVersion</key>
19-
<string>100</string>
5+
<key>CFBundleIdentifier</key>
6+
<string>com.verifyos.BadApp</string>
7+
<key>CFBundleExecutable</key>
8+
<string>BadApp</string>
209
</dict>
2110
</plist>
82.2 KB
Binary file not shown.

examples/Payload/GoodApp.app/Info.plist

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>NSCameraUsageDescription</key>
6-
<string>Camera is used to scan barcodes.</string>
7-
<key>CFBundleIdentifier</key>
8-
<string>com.example.GoodApp</string>
9-
<key>LSApplicationQueriesSchemes</key>
10-
<array>
11-
<string>fb</string>
12-
<string>twitter</string>
13-
</array>
14-
<key>UIRequiredDeviceCapabilities</key>
15-
<array>
16-
<string>arm64</string>
17-
</array>
18-
<key>CFBundleShortVersionString</key>
19-
<string>1.0.0</string>
20-
<key>CFBundleVersion</key>
21-
<string>100</string>
5+
<key>CFBundleIdentifier</key>
6+
<string>com.verifyos.GoodApp</string>
7+
<key>CFBundleExecutable</key>
8+
<string>GoodApp</string>
9+
<key>CFBundleName</key>
10+
<string>GoodApp</string>
11+
<key>CFBundleShortVersionString</key>
12+
<string>1.0.0</string>
13+
<key>CFBundleVersion</key>
14+
<string>1</string>
15+
<key>ITSAppUsesNonExemptEncryption</key>
16+
<false/>
17+
<key>LSApplicationQueriesSchemes</key>
18+
<array/>
19+
<key>UIRequiredDeviceCapabilities</key>
20+
<array>
21+
<string>arm64</string>
22+
</array>
23+
<key>DTXcode</key>
24+
<string>1500</string>
25+
<key>NSCameraUsageDescription</key>
26+
<string>We need camera access to scan QR codes.</string>
2227
</dict>
2328
</plist>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSPrivacyCollectedDataTypes</key>
6+
<array/>
7+
<key>NSPrivacyAccessedAPITypes</key>
8+
<array/>
9+
</dict>
10+
</plist>

examples/bad_app.ipa

7.77 KB
Binary file not shown.

examples/good_app.ipa

8.15 KB
Binary file not shown.

src/commands/lsp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ mod tests {
336336
rule_name,
337337
category: RuleCategory::Other,
338338
severity,
339+
target: "Bundle".to_string(),
339340
recommendation: "",
340341
report: Ok(report),
341342
duration_ms: 1,

src/core/engine.rs

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub struct EngineResult {
2424
pub rule_name: &'static str,
2525
pub category: RuleCategory,
2626
pub severity: Severity,
27+
pub target: String,
2728
pub recommendation: &'static str,
2829
pub report: Result<RuleReport, RuleError>,
2930
pub duration_ms: u128,
@@ -61,52 +62,72 @@ impl Engine {
6162
}
6263

6364
let extracted_ipa = extract_ipa(path)?;
64-
65-
// Attempt to discover project metadata during extraction
66-
let discovered_project_path = extracted_ipa
67-
.get_project_path()
65+
let targets = extracted_ipa
66+
.discover_targets()
6867
.map_err(|e| OrchestratorError::Extraction(ExtractionError::Io(e)))?;
6968

70-
let discovered_project = discovered_project_path.as_ref().and_then(|p| {
71-
if p.extension().is_some_and(|e| e == "xcworkspace") {
72-
crate::parsers::xcworkspace_parser::Xcworkspace::from_path(p)
69+
let mut all_results = Vec::new();
70+
let mut total_cache_stats = ArtifactCacheStats::default();
71+
72+
if targets.is_empty() {
73+
// Fallback to original logic if no specific targets found
74+
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+
// Try to find a project context for this app if it's in a larger folder
81+
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)
7397
.ok()
7498
.and_then(|ws| ws.project_paths.first().cloned())
7599
.and_then(|proj_path| {
76100
crate::parsers::xcode_parser::XcodeProject::from_path(proj_path).ok()
77101
})
78102
} else {
79-
crate::parsers::xcode_parser::XcodeProject::from_path(p).ok()
103+
None
104+
};
105+
106+
let app_results =
107+
self.run_on_bundle_internal(&target_path, run_started, project_context)?;
108+
109+
// Tag results with target name
110+
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);
80118
}
81-
});
82-
83-
let app_bundle_path = extracted_ipa
84-
.get_app_bundle_path()
85-
.map_err(|e| OrchestratorError::Extraction(ExtractionError::Io(e)))?;
86119

87-
let app_bundle_path = match app_bundle_path {
88-
Some(p) => p,
89-
None => {
90-
// If we found a project but no .app, we can still proceed with the extraction root
91-
// as a context and let rules that check project metadata run.
92-
if discovered_project_path.is_some() {
93-
extracted_ipa.payload_dir.clone()
94-
} else {
95-
let mut entries = Vec::new();
96-
if let Ok(rd) = std::fs::read_dir(&extracted_ipa.payload_dir) {
97-
for entry in rd.flatten().take(10) {
98-
entries.push(entry.file_name().to_string_lossy().into_owned());
99-
}
100-
}
101-
return Err(OrchestratorError::AppBundleNotFoundWithContext(
102-
extracted_ipa.payload_dir.display().to_string(),
103-
entries.join(", "),
104-
));
105-
}
106-
}
107-
};
120+
// Merge stats (simplified)
121+
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+
// ... other stats could be merged if needed, but for now we focus on results
124+
}
108125

109-
self.run_on_bundle_internal(&app_bundle_path, run_started, discovered_project)
126+
Ok(EngineRun {
127+
results: all_results,
128+
total_duration_ms: run_started.elapsed().as_millis(),
129+
cache_stats: total_cache_stats,
130+
})
110131
}
111132

112133
pub fn run_on_bundle<P: AsRef<Path>>(
@@ -147,6 +168,10 @@ impl Engine {
147168
rule_name: rule.name(),
148169
category: rule.category(),
149170
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()),
150175
recommendation: rule.recommendation(),
151176
report: res,
152177
duration_ms: rule_started.elapsed().as_millis(),

src/parsers/zip_extractor.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,33 @@ impl ExtractedIpa {
7777

7878
Ok(project)
7979
}
80+
81+
pub fn discover_targets(&self) -> io::Result<Vec<(PathBuf, String)>> {
82+
let mut targets = Vec::new();
83+
let mut queue = vec![self.payload_dir.clone()];
84+
85+
while let Some(dir) = queue.pop() {
86+
if let Ok(entries) = fs::read_dir(dir) {
87+
for entry in entries.flatten() {
88+
let path = entry.path();
89+
if path.is_dir() {
90+
let extension = path.extension().and_then(|e| e.to_str());
91+
match extension {
92+
Some("app") => targets.push((path.clone(), "app".to_string())),
93+
Some("xcodeproj") => targets.push((path.clone(), "project".to_string())),
94+
Some("xcworkspace") => {
95+
targets.push((path.clone(), "workspace".to_string()))
96+
}
97+
_ => queue.push(path),
98+
}
99+
} else if path.extension().and_then(|e| e.to_str()) == Some("ipa") {
100+
targets.push((path.clone(), "ipa".to_string()));
101+
}
102+
}
103+
}
104+
}
105+
Ok(targets)
106+
}
80107
}
81108

82109
pub fn extract_ipa<P: AsRef<Path>>(ipa_path: P) -> Result<ExtractedIpa, ExtractionError> {

0 commit comments

Comments
 (0)