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