verifyos_cli/rules/
bundle_metadata.rs1use crate::parsers::bundle_scanner::find_nested_bundles;
2use crate::parsers::plist_reader::InfoPlist;
3use crate::rules::core::{
4 AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
5};
6
7pub struct BundleMetadataConsistencyRule;
8
9impl AppStoreRule for BundleMetadataConsistencyRule {
10 fn id(&self) -> &'static str {
11 "RULE_BUNDLE_METADATA_CONSISTENCY"
12 }
13
14 fn name(&self) -> &'static str {
15 "Bundle Metadata Consistency"
16 }
17
18 fn category(&self) -> RuleCategory {
19 RuleCategory::Metadata
20 }
21
22 fn severity(&self) -> Severity {
23 Severity::Warning
24 }
25
26 fn recommendation(&self) -> &'static str {
27 "Align CFBundleIdentifier and versioning across app and extensions."
28 }
29
30 fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
31 let Some(app_plist) = artifact.info_plist else {
32 return Ok(RuleReport {
33 status: RuleStatus::Skip,
34 message: Some("Info.plist not found".to_string()),
35 evidence: None,
36 });
37 };
38
39 let app_bundle_id = app_plist.get_string("CFBundleIdentifier");
40 let app_short_version = app_plist.get_string("CFBundleShortVersionString");
41 let app_build_version = app_plist.get_string("CFBundleVersion");
42
43 let bundles = find_nested_bundles(artifact.app_bundle_path)
44 .map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
45
46 if bundles.is_empty() {
47 return Ok(RuleReport {
48 status: RuleStatus::Pass,
49 message: Some("No nested bundles found".to_string()),
50 evidence: None,
51 });
52 }
53
54 let mut mismatches = Vec::new();
55
56 for bundle in bundles {
57 if !bundle
58 .bundle_path
59 .extension()
60 .and_then(|e| e.to_str())
61 .map(|ext| ext == "appex" || ext == "app")
62 .unwrap_or(false)
63 {
64 continue;
65 }
66
67 let plist_path = bundle.bundle_path.join("Info.plist");
68 if !plist_path.exists() {
69 continue;
70 }
71
72 let plist = match InfoPlist::from_file(&plist_path) {
73 Ok(plist) => plist,
74 Err(_) => continue,
75 };
76
77 if let (Some(app_id), Some(child_id)) =
78 (app_bundle_id, plist.get_string("CFBundleIdentifier"))
79 {
80 if !child_id.starts_with(app_id) {
81 mismatches.push(format!(
82 "{}: CFBundleIdentifier {} not under {}",
83 bundle.display_name, child_id, app_id
84 ));
85 }
86 }
87
88 if let (Some(app_short), Some(child_short)) = (
89 app_short_version,
90 plist.get_string("CFBundleShortVersionString"),
91 ) {
92 if child_short != app_short {
93 mismatches.push(format!(
94 "{}: CFBundleShortVersionString {} != {}",
95 bundle.display_name, child_short, app_short
96 ));
97 }
98 }
99
100 if let (Some(app_build), Some(child_build)) =
101 (app_build_version, plist.get_string("CFBundleVersion"))
102 {
103 if child_build != app_build {
104 mismatches.push(format!(
105 "{}: CFBundleVersion {} != {}",
106 bundle.display_name, child_build, app_build
107 ));
108 }
109 }
110 }
111
112 if mismatches.is_empty() {
113 return Ok(RuleReport {
114 status: RuleStatus::Pass,
115 message: None,
116 evidence: None,
117 });
118 }
119
120 Ok(RuleReport {
121 status: RuleStatus::Fail,
122 message: Some("Bundle metadata mismatches".to_string()),
123 evidence: Some(mismatches.join("; ")),
124 })
125 }
126}