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, 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(artifact: &ArtifactContext, limit: usize) -> Vec<String> {
49    let mut hits = Vec::new();
50
51    for path in artifact.bundle_file_paths() {
52        if is_sensitive_path(&path) {
53            let display = match path.strip_prefix(artifact.app_bundle_path) {
54                Ok(rel) => rel.display().to_string(),
55                Err(_) => path.display().to_string(),
56            };
57            hits.push(display);
58            if hits.len() >= limit {
59                return hits;
60            }
61        }
62    }
63
64    hits
65}
66
67fn is_sensitive_path(path: &Path) -> bool {
68    let name = path
69        .file_name()
70        .and_then(|n| n.to_str())
71        .unwrap_or("")
72        .to_ascii_lowercase();
73
74    if name == ".env" || name.ends_with(".env") {
75        return true;
76    }
77
78    if matches!(
79        path.extension().and_then(|e| e.to_str()).map(|s| s.to_ascii_lowercase()),
80        Some(ext) if ext == "p12" || ext == "pfx" || ext == "pem" || ext == "key"
81    ) {
82        return true;
83    }
84
85    if name == "embedded.mobileprovision" {
86        return true;
87    }
88
89    if name.ends_with(".mobileprovision") {
90        return true;
91    }
92
93    if name.contains("secret") || name.contains("apikey") || name.contains("api_key") {
94        return true;
95    }
96
97    false
98}