verifyos_cli/rules/
bundle_leakage.rs1use 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}