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, 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}