Skip to main content

verifyos_cli/rules/
bundle_metadata.rs

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