Skip to main content

verifyos_cli/rules/
nested_bundles.rs

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