Skip to main content

verifyos_cli/rules/
bundle_leakage.rs

1use crate::rules::core::{
2    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4use std::path::Path;
5
6pub struct BundleResourceLeakageRule;
7
8impl AppStoreRule for BundleResourceLeakageRule {
9    fn id(&self) -> &'static str {
10        "RULE_BUNDLE_RESOURCE_LEAKAGE"
11    }
12
13    fn name(&self) -> &'static str {
14        "Sensitive Files in Bundle"
15    }
16
17    fn category(&self) -> RuleCategory {
18        RuleCategory::Bundling
19    }
20
21    fn severity(&self) -> Severity {
22        Severity::Error
23    }
24
25    fn recommendation(&self) -> &'static str {
26        "Remove certificates, provisioning profiles, or env files from the app bundle before submission."
27    }
28
29    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
30        let offenders = scan_bundle_for_sensitive_files(artifact.app_bundle_path, 80)?;
31
32        if offenders.is_empty() {
33            return Ok(RuleReport {
34                status: RuleStatus::Pass,
35                message: Some("No sensitive files found in bundle".to_string()),
36                evidence: None,
37            });
38        }
39
40        Ok(RuleReport {
41            status: RuleStatus::Fail,
42            message: Some("Sensitive files found in bundle".to_string()),
43            evidence: Some(offenders.join(" | ")),
44        })
45    }
46}
47
48fn scan_bundle_for_sensitive_files(
49    app_bundle_path: &Path,
50    limit: usize,
51) -> Result<Vec<String>, RuleError> {
52    let mut hits = Vec::new();
53    let mut stack = vec![app_bundle_path.to_path_buf()];
54
55    while let Some(path) = stack.pop() {
56        let entries = match std::fs::read_dir(&path) {
57            Ok(entries) => entries,
58            Err(_) => continue,
59        };
60
61        for entry in entries {
62            let entry = match entry {
63                Ok(entry) => entry,
64                Err(_) => continue,
65            };
66            let path = entry.path();
67
68            if path.is_dir() {
69                stack.push(path);
70                continue;
71            }
72
73            if is_sensitive_path(&path) {
74                let display = match path.strip_prefix(app_bundle_path) {
75                    Ok(rel) => rel.display().to_string(),
76                    Err(_) => path.display().to_string(),
77                };
78                hits.push(display);
79                if hits.len() >= limit {
80                    return Ok(hits);
81                }
82            }
83        }
84    }
85
86    Ok(hits)
87}
88
89fn is_sensitive_path(path: &Path) -> bool {
90    let name = path
91        .file_name()
92        .and_then(|n| n.to_str())
93        .unwrap_or("")
94        .to_ascii_lowercase();
95
96    if name == ".env" || name.ends_with(".env") {
97        return true;
98    }
99
100    if matches!(
101        path.extension().and_then(|e| e.to_str()).map(|s| s.to_ascii_lowercase()),
102        Some(ext) if ext == "p12" || ext == "pfx" || ext == "pem" || ext == "key"
103    ) {
104        return true;
105    }
106
107    if name == "embedded.mobileprovision" {
108        return true;
109    }
110
111    if name.ends_with(".mobileprovision") {
112        return true;
113    }
114
115    if name.contains("secret") || name.contains("apikey") || name.contains("api_key") {
116        return true;
117    }
118
119    false
120}