verifyos_cli/rules/
nested_bundles.rs1use 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}