Skip to main content

verifyos_cli/rules/
nested_bundles.rs

1use crate::rules::core::{
2    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4
5pub struct NestedBundleEntitlementsRule;
6
7impl AppStoreRule for NestedBundleEntitlementsRule {
8    fn id(&self) -> &'static str {
9        "RULE_NESTED_ENTITLEMENTS_MISMATCH"
10    }
11
12    fn name(&self) -> &'static str {
13        "Nested Bundle Entitlements Mismatch"
14    }
15
16    fn category(&self) -> RuleCategory {
17        RuleCategory::Entitlements
18    }
19
20    fn severity(&self) -> Severity {
21        Severity::Error
22    }
23
24    fn recommendation(&self) -> &'static str {
25        "Ensure embedded bundles have entitlements matching their embedded provisioning profiles."
26    }
27
28    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
29        let bundles = artifact
30            .nested_bundles()
31            .map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
32
33        if bundles.is_empty() {
34            return Ok(RuleReport {
35                status: RuleStatus::Pass,
36                message: Some("No nested bundles found".to_string()),
37                evidence: None,
38            });
39        }
40
41        let mut mismatches = Vec::new();
42
43        for bundle in bundles {
44            let Some(entitlements) = artifact.entitlements_for_bundle(&bundle.bundle_path)? else {
45                continue;
46            };
47            let Some(profile) = artifact
48                .provisioning_profile_for_bundle(&bundle.bundle_path)
49                .map_err(RuleError::Provisioning)?
50            else {
51                continue;
52            };
53
54            let mut local_mismatches = Vec::new();
55
56            if let Some(app_aps) = entitlements.get_string("aps-environment") {
57                match profile.entitlements.get_string("aps-environment") {
58                    Some(prov_aps) if prov_aps != app_aps => local_mismatches.push(format!(
59                        "aps-environment app={} profile={}",
60                        app_aps, prov_aps
61                    )),
62                    None => local_mismatches.push("aps-environment missing in profile".to_string()),
63                    _ => {}
64                }
65            }
66
67            let keychain_diff = diff_string_array(
68                &entitlements,
69                &profile.entitlements,
70                "keychain-access-groups",
71            );
72            if !keychain_diff.is_empty() {
73                local_mismatches.push(format!(
74                    "keychain-access-groups missing in profile: {}",
75                    keychain_diff.join(", ")
76                ));
77            }
78
79            let icloud_diff = diff_string_array(
80                &entitlements,
81                &profile.entitlements,
82                "com.apple.developer.icloud-container-identifiers",
83            );
84            if !icloud_diff.is_empty() {
85                local_mismatches.push(format!(
86                    "iCloud containers missing in profile: {}",
87                    icloud_diff.join(", ")
88                ));
89            }
90
91            if !local_mismatches.is_empty() {
92                mismatches.push(format!(
93                    "{}: {}",
94                    bundle.display_name,
95                    local_mismatches.join("; ")
96                ));
97            }
98        }
99
100        if mismatches.is_empty() {
101            return Ok(RuleReport {
102                status: RuleStatus::Pass,
103                message: Some("Nested bundle entitlements match profiles".to_string()),
104                evidence: None,
105            });
106        }
107
108        Ok(RuleReport {
109            status: RuleStatus::Fail,
110            message: Some("Nested bundle entitlements mismatch".to_string()),
111            evidence: Some(mismatches.join(" | ")),
112        })
113    }
114}
115
116pub struct NestedBundleDebugEntitlementRule;
117
118impl AppStoreRule for NestedBundleDebugEntitlementRule {
119    fn id(&self) -> &'static str {
120        "RULE_NESTED_DEBUG_ENTITLEMENT"
121    }
122
123    fn name(&self) -> &'static str {
124        "Nested Bundle Debug Entitlement"
125    }
126
127    fn category(&self) -> RuleCategory {
128        RuleCategory::Entitlements
129    }
130
131    fn severity(&self) -> Severity {
132        Severity::Error
133    }
134
135    fn recommendation(&self) -> &'static str {
136        "Remove get-task-allow from embedded frameworks/extensions."
137    }
138
139    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
140        let bundles = artifact
141            .nested_bundles()
142            .map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
143
144        if bundles.is_empty() {
145            return Ok(RuleReport {
146                status: RuleStatus::Pass,
147                message: Some("No nested bundles found".to_string()),
148                evidence: None,
149            });
150        }
151
152        let mut offenders = Vec::new();
153
154        for bundle in bundles {
155            let Some(entitlements) = artifact.entitlements_for_bundle(&bundle.bundle_path)? else {
156                continue;
157            };
158
159            if let Some(true) = entitlements.get_bool("get-task-allow") {
160                offenders.push(bundle.display_name);
161            }
162        }
163
164        if offenders.is_empty() {
165            return Ok(RuleReport {
166                status: RuleStatus::Pass,
167                message: Some("No debug entitlements in nested bundles".to_string()),
168                evidence: None,
169            });
170        }
171
172        Ok(RuleReport {
173            status: RuleStatus::Fail,
174            message: Some("Debug entitlements found in nested bundles".to_string()),
175            evidence: Some(offenders.join(", ")),
176        })
177    }
178}
179
180fn diff_string_array(
181    entitlements: &crate::parsers::plist_reader::InfoPlist,
182    profile: &crate::parsers::plist_reader::InfoPlist,
183    key: &str,
184) -> Vec<String> {
185    let app_values = entitlements.get_array_strings(key).unwrap_or_default();
186    let profile_values = profile.get_array_strings(key).unwrap_or_default();
187
188    app_values
189        .into_iter()
190        .filter(|value| !profile_values.contains(value))
191        .collect()
192}