use crate::parsers::plist_reader::InfoPlist;
use crate::rules::core::{
AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
};
pub struct EmbeddedCodeSignatureTeamRule;
impl AppStoreRule for EmbeddedCodeSignatureTeamRule {
fn id(&self) -> &'static str {
"RULE_EMBEDDED_TEAM_ID_MISMATCH"
}
fn name(&self) -> &'static str {
"Embedded Team ID Mismatch"
}
fn category(&self) -> RuleCategory {
RuleCategory::Signing
}
fn severity(&self) -> Severity {
Severity::Error
}
fn recommendation(&self) -> &'static str {
"Ensure all embedded frameworks/extensions are signed with the same Team ID as the app binary."
}
fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
let info_plist = match artifact.info_plist {
Some(plist) => plist,
None => match artifact.bundle_info_plist(artifact.app_bundle_path) {
Ok(Some(plist)) => return evaluate_with_plist(artifact, &plist),
Ok(None) => {
return Ok(RuleReport {
status: RuleStatus::Skip,
message: Some("Info.plist not found".to_string()),
evidence: None,
})
}
Err(err) => {
let plist_path = artifact.app_bundle_path.join("Info.plist");
return Ok(RuleReport {
status: RuleStatus::Skip,
message: Some(format!("Failed to parse Info.plist: {err}")),
evidence: Some(plist_path.display().to_string()),
});
}
},
};
evaluate_with_plist(artifact, info_plist)
}
}
fn evaluate_with_plist(
artifact: &ArtifactContext,
info_plist: &InfoPlist,
) -> Result<RuleReport, RuleError> {
let Some(app_executable) = info_plist.get_string("CFBundleExecutable") else {
return Ok(RuleReport {
status: RuleStatus::Skip,
message: Some("CFBundleExecutable not found".to_string()),
evidence: None,
});
};
let app_executable_path = artifact.app_bundle_path.join(app_executable);
if !app_executable_path.exists() {
return Ok(RuleReport {
status: RuleStatus::Skip,
message: Some("App executable not found".to_string()),
evidence: Some(app_executable_path.display().to_string()),
});
}
let app_summary = artifact
.signature_summary(&app_executable_path)
.map_err(RuleError::MachO)?;
if app_summary.total_slices == 0 {
return Ok(RuleReport {
status: RuleStatus::Skip,
message: Some("No Mach-O slices found".to_string()),
evidence: Some(app_executable_path.display().to_string()),
});
}
if app_summary.signed_slices == 0 {
return Ok(RuleReport {
status: RuleStatus::Fail,
message: Some("App executable missing code signature".to_string()),
evidence: Some(app_executable_path.display().to_string()),
});
}
if app_summary.signed_slices < app_summary.total_slices {
return Ok(RuleReport {
status: RuleStatus::Fail,
message: Some("App executable has unsigned slices".to_string()),
evidence: Some(app_executable_path.display().to_string()),
});
}
let Some(app_team_id) = app_summary.team_id else {
return Ok(RuleReport {
status: RuleStatus::Fail,
message: Some("App executable missing Team ID".to_string()),
evidence: Some(app_executable_path.display().to_string()),
});
};
let bundles = artifact
.nested_bundles()
.map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
if bundles.is_empty() {
return Ok(RuleReport {
status: RuleStatus::Pass,
message: Some("No embedded bundles found".to_string()),
evidence: None,
});
}
let mut mismatches = Vec::new();
for bundle in bundles {
let Some(executable_path) = artifact.executable_path_for_bundle(&bundle.bundle_path) else {
mismatches.push(format!(
"{}: Missing CFBundleExecutable",
bundle.display_name
));
continue;
};
if !executable_path.exists() {
mismatches.push(format!(
"{}: Executable not found at {}",
bundle.display_name,
executable_path.display()
));
continue;
}
let summary = artifact
.signature_summary(&executable_path)
.map_err(RuleError::MachO)?;
if summary.total_slices == 0 {
mismatches.push(format!("{}: No Mach-O slices found", bundle.display_name));
continue;
}
if summary.signed_slices == 0 {
mismatches.push(format!("{}: Missing code signature", bundle.display_name));
continue;
}
if summary.signed_slices < summary.total_slices {
mismatches.push(format!("{}: Unsigned Mach-O slices", bundle.display_name));
continue;
}
let Some(team_id) = summary.team_id else {
mismatches.push(format!("{}: Missing Team ID", bundle.display_name));
continue;
};
if team_id != app_team_id {
mismatches.push(format!(
"{}: Team ID mismatch ({} != {})",
bundle.display_name, team_id, app_team_id
));
}
}
if mismatches.is_empty() {
return Ok(RuleReport {
status: RuleStatus::Pass,
message: Some("Embedded bundles share the same Team ID".to_string()),
evidence: None,
});
}
Ok(RuleReport {
status: RuleStatus::Fail,
message: Some("Embedded bundle signing mismatch".to_string()),
evidence: Some(mismatches.join(" | ")),
})
}