use crate::parsers::bundle_scanner::{find_nested_bundles, BundleScanError, BundleTarget};
use crate::parsers::macho_parser::{
read_macho_signature_summary, MachOError, MachOExecutable, MachOSignatureSummary,
};
use crate::parsers::macho_scanner::{
scan_capabilities_from_app_bundle, scan_private_api_from_app_bundle, scan_sdks_from_app_bundle,
scan_usage_from_app_bundle, CapabilityScan, PrivateApiScan, SdkScan, UsageScan, UsageScanError,
};
use crate::parsers::plist_reader::{InfoPlist, PlistError};
use crate::parsers::provisioning_profile::{ProvisioningError, ProvisioningProfile};
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
pub const RULESET_VERSION: &str = "0.1.0";
#[derive(Debug, thiserror::Error, Diagnostic)]
pub enum RuleError {
#[error(transparent)]
#[diagnostic(transparent)]
Entitlements(#[from] crate::rules::entitlements::EntitlementsError),
#[error(transparent)]
#[diagnostic(transparent)]
Provisioning(#[from] crate::parsers::provisioning_profile::ProvisioningError),
#[error(transparent)]
#[diagnostic(transparent)]
MachO(#[from] crate::parsers::macho_parser::MachOError),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum RuleStatus {
Pass,
Fail,
Error,
Skip,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleReport {
pub status: RuleStatus,
pub message: Option<String>,
pub evidence: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CacheCounter {
pub hits: u64,
pub misses: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArtifactCacheStats {
pub nested_bundles: CacheCounter,
pub usage_scan: CacheCounter,
pub private_api_scan: CacheCounter,
pub sdk_scan: CacheCounter,
pub capability_scan: CacheCounter,
pub signature_summary: CacheCounter,
pub bundle_plist: CacheCounter,
pub entitlements: CacheCounter,
pub provisioning_profile: CacheCounter,
pub bundle_files: CacheCounter,
pub instrumentation_scan: CacheCounter,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum RuleCategory {
Privacy,
Signing,
Bundling,
Entitlements,
Ats,
ThirdParty,
Permissions,
Metadata,
Other,
}
pub struct ArtifactContext<'a> {
pub app_bundle_path: &'a std::path::Path,
pub info_plist: Option<&'a crate::parsers::plist_reader::InfoPlist>,
nested_bundles_cache: Mutex<Option<Vec<BundleTarget>>>,
usage_scan_cache: Mutex<Option<UsageScan>>,
private_api_scan_cache: Mutex<Option<PrivateApiScan>>,
sdk_scan_cache: Mutex<Option<SdkScan>>,
capability_scan_cache: Mutex<Option<CapabilityScan>>,
signature_summary_cache: Mutex<HashMap<PathBuf, MachOSignatureSummary>>,
bundle_plist_cache: Mutex<HashMap<PathBuf, Option<InfoPlist>>>,
entitlements_cache: Mutex<HashMap<PathBuf, Option<InfoPlist>>>,
provisioning_profile_cache: Mutex<HashMap<PathBuf, Option<ProvisioningProfile>>>,
bundle_file_cache: Mutex<Option<Vec<PathBuf>>>,
instrumentation_scan_cache: Mutex<Option<Vec<&'static str>>>,
pub xcode_project: Option<&'a crate::parsers::xcode_parser::XcodeProject>,
cache_stats: Mutex<ArtifactCacheStats>,
}
impl<'a> ArtifactContext<'a> {
pub fn new(
app_bundle_path: &'a Path,
info_plist: Option<&'a crate::parsers::plist_reader::InfoPlist>,
xcode_project: Option<&'a crate::parsers::xcode_parser::XcodeProject>,
) -> Self {
Self {
app_bundle_path,
info_plist,
nested_bundles_cache: Mutex::new(None),
usage_scan_cache: Mutex::new(None),
private_api_scan_cache: Mutex::new(None),
sdk_scan_cache: Mutex::new(None),
capability_scan_cache: Mutex::new(None),
signature_summary_cache: Mutex::new(HashMap::new()),
bundle_plist_cache: Mutex::new(HashMap::new()),
entitlements_cache: Mutex::new(HashMap::new()),
provisioning_profile_cache: Mutex::new(HashMap::new()),
bundle_file_cache: Mutex::new(None),
instrumentation_scan_cache: Mutex::new(None),
xcode_project,
cache_stats: Mutex::new(ArtifactCacheStats::default()),
}
}
pub fn nested_bundles(&self) -> Result<Vec<BundleTarget>, BundleScanError> {
if let Some(bundles) = self.nested_bundles_cache.lock().unwrap().as_ref() {
self.cache_stats.lock().unwrap().nested_bundles.hits += 1;
return Ok(bundles.clone());
}
self.cache_stats.lock().unwrap().nested_bundles.misses += 1;
let bundles = find_nested_bundles(self.app_bundle_path)?;
*self.nested_bundles_cache.lock().unwrap() = Some(bundles.clone());
Ok(bundles)
}
pub fn usage_scan(&self) -> Result<UsageScan, UsageScanError> {
if let Some(scan) = self.usage_scan_cache.lock().unwrap().as_ref() {
self.cache_stats.lock().unwrap().usage_scan.hits += 1;
return Ok(scan.clone());
}
self.cache_stats.lock().unwrap().usage_scan.misses += 1;
let scan = scan_usage_from_app_bundle(self.app_bundle_path)?;
*self.usage_scan_cache.lock().unwrap() = Some(scan.clone());
Ok(scan)
}
pub fn private_api_scan(&self) -> Result<PrivateApiScan, UsageScanError> {
if let Some(scan) = self.private_api_scan_cache.lock().unwrap().as_ref() {
self.cache_stats.lock().unwrap().private_api_scan.hits += 1;
return Ok(scan.clone());
}
self.cache_stats.lock().unwrap().private_api_scan.misses += 1;
let scan = scan_private_api_from_app_bundle(self.app_bundle_path)?;
*self.private_api_scan_cache.lock().unwrap() = Some(scan.clone());
Ok(scan)
}
pub fn sdk_scan(&self) -> Result<SdkScan, UsageScanError> {
if let Some(scan) = self.sdk_scan_cache.lock().unwrap().as_ref() {
self.cache_stats.lock().unwrap().sdk_scan.hits += 1;
return Ok(scan.clone());
}
self.cache_stats.lock().unwrap().sdk_scan.misses += 1;
let scan = scan_sdks_from_app_bundle(self.app_bundle_path)?;
*self.sdk_scan_cache.lock().unwrap() = Some(scan.clone());
Ok(scan)
}
pub fn capability_scan(&self) -> Result<CapabilityScan, UsageScanError> {
if let Some(scan) = self.capability_scan_cache.lock().unwrap().as_ref() {
self.cache_stats.lock().unwrap().capability_scan.hits += 1;
return Ok(scan.clone());
}
self.cache_stats.lock().unwrap().capability_scan.misses += 1;
let scan = scan_capabilities_from_app_bundle(self.app_bundle_path)?;
*self.capability_scan_cache.lock().unwrap() = Some(scan.clone());
Ok(scan)
}
pub fn instrumentation_scan(&self) -> Result<Vec<&'static str>, UsageScanError> {
if let Some(hits) = self.instrumentation_scan_cache.lock().unwrap().as_ref() {
self.cache_stats.lock().unwrap().instrumentation_scan.hits += 1;
return Ok(hits.clone());
}
self.cache_stats.lock().unwrap().instrumentation_scan.misses += 1;
let hits = crate::parsers::macho_scanner::scan_instrumentation_from_app_bundle(
self.app_bundle_path,
)?;
*self.instrumentation_scan_cache.lock().unwrap() = Some(hits.clone());
Ok(hits)
}
pub fn signature_summary(
&self,
executable_path: impl AsRef<Path>,
) -> Result<MachOSignatureSummary, MachOError> {
let executable_path = executable_path.as_ref().to_path_buf();
if let Some(summary) = self
.signature_summary_cache
.lock()
.unwrap()
.get(&executable_path)
{
self.cache_stats.lock().unwrap().signature_summary.hits += 1;
return Ok(summary.clone());
}
self.cache_stats.lock().unwrap().signature_summary.misses += 1;
let summary = read_macho_signature_summary(&executable_path)?;
self.signature_summary_cache
.lock()
.unwrap()
.insert(executable_path, summary.clone());
Ok(summary)
}
pub fn executable_path_for_bundle(&self, bundle_path: &Path) -> Option<PathBuf> {
if let Ok(Some(plist)) = self.bundle_info_plist(bundle_path) {
if let Some(executable) = plist.get_string("CFBundleExecutable") {
let candidate = bundle_path.join(executable);
if candidate.exists() {
return Some(candidate);
}
}
}
resolve_bundle_executable_path(bundle_path)
}
pub fn bundle_info_plist(&self, bundle_path: &Path) -> Result<Option<InfoPlist>, PlistError> {
if let Some(plist) = self.bundle_plist_cache.lock().unwrap().get(bundle_path) {
self.cache_stats.lock().unwrap().bundle_plist.hits += 1;
return Ok(plist.clone());
}
self.cache_stats.lock().unwrap().bundle_plist.misses += 1;
let plist_path = bundle_path.join("Info.plist");
let plist = if plist_path.exists() {
Some(InfoPlist::from_file(&plist_path)?)
} else {
None
};
self.bundle_plist_cache
.lock()
.unwrap()
.insert(bundle_path.to_path_buf(), plist.clone());
Ok(plist)
}
pub fn entitlements_for_bundle(
&self,
bundle_path: &Path,
) -> Result<Option<InfoPlist>, RuleError> {
let executable_path = match self.executable_path_for_bundle(bundle_path) {
Some(path) => path,
None => return Ok(None),
};
if let Some(entitlements) = self
.entitlements_cache
.lock()
.unwrap()
.get(&executable_path)
{
self.cache_stats.lock().unwrap().entitlements.hits += 1;
return Ok(entitlements.clone());
}
self.cache_stats.lock().unwrap().entitlements.misses += 1;
let macho = MachOExecutable::from_file(&executable_path)
.map_err(crate::rules::entitlements::EntitlementsError::MachO)
.map_err(RuleError::Entitlements)?;
let entitlements = match macho.entitlements {
Some(entitlements_xml) => {
let plist = InfoPlist::from_bytes(entitlements_xml.as_bytes())
.map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
Some(plist)
}
None => None,
};
self.entitlements_cache
.lock()
.unwrap()
.insert(executable_path, entitlements.clone());
Ok(entitlements)
}
pub fn provisioning_profile_for_bundle(
&self,
bundle_path: &Path,
) -> Result<Option<ProvisioningProfile>, ProvisioningError> {
let provisioning_path = bundle_path.join("embedded.mobileprovision");
if let Some(profile) = self
.provisioning_profile_cache
.lock()
.unwrap()
.get(&provisioning_path)
{
self.cache_stats.lock().unwrap().provisioning_profile.hits += 1;
return Ok(profile.clone());
}
self.cache_stats.lock().unwrap().provisioning_profile.misses += 1;
let profile = if provisioning_path.exists() {
Some(ProvisioningProfile::from_embedded_file(&provisioning_path)?)
} else {
None
};
self.provisioning_profile_cache
.lock()
.unwrap()
.insert(provisioning_path, profile.clone());
Ok(profile)
}
pub fn bundle_file_paths(&self) -> Vec<PathBuf> {
if let Some(paths) = self.bundle_file_cache.lock().unwrap().as_ref() {
self.cache_stats.lock().unwrap().bundle_files.hits += 1;
return paths.clone();
}
self.cache_stats.lock().unwrap().bundle_files.misses += 1;
let mut files = Vec::new();
collect_bundle_files(self.app_bundle_path, &mut files);
*self.bundle_file_cache.lock().unwrap() = Some(files.clone());
files
}
pub fn bundle_relative_file(&self, relative_path: &str) -> Option<PathBuf> {
self.bundle_file_paths().into_iter().find(|path| {
path.strip_prefix(self.app_bundle_path)
.ok()
.map(|rel| rel == Path::new(relative_path))
.unwrap_or(false)
})
}
pub fn cache_stats(&self) -> ArtifactCacheStats {
self.cache_stats.lock().unwrap().clone()
}
}
fn resolve_bundle_executable_path(bundle_path: &Path) -> Option<PathBuf> {
let bundle_name = bundle_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.trim_end_matches(".app")
.trim_end_matches(".appex")
.trim_end_matches(".framework");
if bundle_name.is_empty() {
return None;
}
let fallback = bundle_path.join(bundle_name);
if fallback.exists() {
Some(fallback)
} else {
None
}
}
fn collect_bundle_files(root: &Path, files: &mut Vec<PathBuf>) {
let entries = match std::fs::read_dir(root) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_bundle_files(&path, files);
} else {
files.push(path);
}
}
}
pub trait AppStoreRule: Send + Sync {
fn id(&self) -> &'static str;
fn name(&self) -> &'static str;
fn category(&self) -> RuleCategory;
fn severity(&self) -> Severity;
fn recommendation(&self) -> &'static str;
fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError>;
}