verifyos-cli 0.13.1

AI agent-friendly Rust CLI for scanning iOS app bundles for App Store rejection risks before submission.
Documentation
use crate::core::engine::Engine;
use crate::rules::app_icon::AppIconAlphaRule;
use crate::rules::ats::{AtsAuditRule, AtsExceptionsGranularityRule};
use crate::rules::binary_stripping::BinaryStrippingRule;
use crate::rules::bitcode::BitcodeRule;
use crate::rules::bundle_leakage::BundleResourceLeakageRule;
use crate::rules::bundle_metadata::BundleMetadataConsistencyRule;
use crate::rules::core::AppStoreRule;
use crate::rules::core::{RuleCategory, Severity};
use crate::rules::deprecated_api::DeprecatedApiRule;
use crate::rules::entitlements::{EntitlementsMismatchRule, EntitlementsProvisioningMismatchRule};
use crate::rules::export_compliance::ExportComplianceRule;
use crate::rules::extensions::ExtensionEntitlementsCompatibilityRule;
use crate::rules::info_plist::{
    InfoPlistCapabilitiesRule, InfoPlistRequiredKeysRule, InfoPlistVersionConsistencyRule,
    LSApplicationQueriesSchemesAuditRule, UIRequiredDeviceCapabilitiesAuditRule,
    UsageDescriptionsRule, UsageDescriptionsValueRule,
};
use crate::rules::launch_screen::LaunchScreenStoryboardRule;
use crate::rules::os_version::OSVersionConsistencyRule;
use crate::rules::permissions::CameraUsageDescriptionRule;
use crate::rules::privacy::MissingPrivacyManifestRule;
use crate::rules::privacy_manifest::PrivacyManifestCompletenessRule;
use crate::rules::privacy_sdk::PrivacyManifestSdkCrossCheckRule;
use crate::rules::private_api::PrivateApiRule;
use crate::rules::signing::EmbeddedCodeSignatureTeamRule;
use crate::rules::xcode_requirements::XcodeVersionRule;
use clap::ValueEnum;
use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::HashSet;

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ScanProfile {
    Basic,
    Full,
}

#[derive(Debug, Clone, Default)]
pub struct RuleSelection {
    pub include: HashSet<String>,
    pub exclude: HashSet<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct RuleInventoryItem {
    pub rule_id: String,
    pub name: String,
    pub severity: Severity,
    pub category: RuleCategory,
    pub default_profiles: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct RuleDetailItem {
    pub rule_id: String,
    pub name: String,
    pub severity: Severity,
    pub category: RuleCategory,
    pub recommendation: String,
    pub default_profiles: Vec<String>,
}

impl RuleSelection {
    pub fn allows(&self, rule_id: &str) -> bool {
        let normalized = normalize_rule_id(rule_id);
        let included = self.include.is_empty() || self.include.contains(&normalized);
        let excluded = self.exclude.contains(&normalized);
        included && !excluded
    }
}

pub fn register_rules(engine: &mut Engine, profile: ScanProfile, selection: &RuleSelection) {
    for rule in profile_rules(profile) {
        if selection.allows(rule.id()) {
            engine.register_rule(rule);
        }
    }
}

pub fn available_rule_ids(profile: ScanProfile) -> Vec<String> {
    let mut ids: Vec<String> = profile_rules(profile)
        .into_iter()
        .map(|rule| normalize_rule_id(rule.id()))
        .collect();
    ids.sort();
    ids.dedup();
    ids
}

pub fn normalize_rule_id(rule_id: &str) -> String {
    rule_id.trim().to_ascii_uppercase()
}

pub fn rule_inventory() -> Vec<RuleInventoryItem> {
    let mut items: BTreeMap<String, RuleInventoryItem> = BTreeMap::new();

    for (profile_name, profile) in [("basic", ScanProfile::Basic), ("full", ScanProfile::Full)] {
        for rule in profile_rules(profile) {
            let rule_id = normalize_rule_id(rule.id());
            let entry = items
                .entry(rule_id.clone())
                .or_insert_with(|| RuleInventoryItem {
                    rule_id,
                    name: rule.name().to_string(),
                    severity: rule.severity(),
                    category: rule.category(),
                    default_profiles: Vec::new(),
                });

            if !entry
                .default_profiles
                .iter()
                .any(|name| name == profile_name)
            {
                entry.default_profiles.push(profile_name.to_string());
            }
        }
    }

    items.into_values().collect()
}

pub fn rule_detail(rule_id: &str) -> Option<RuleDetailItem> {
    let normalized = normalize_rule_id(rule_id);
    let mut detail: Option<RuleDetailItem> = None;

    for (profile_name, profile) in [("basic", ScanProfile::Basic), ("full", ScanProfile::Full)] {
        for rule in profile_rules(profile) {
            if normalize_rule_id(rule.id()) != normalized {
                continue;
            }

            let entry = detail.get_or_insert_with(|| RuleDetailItem {
                rule_id: normalize_rule_id(rule.id()),
                name: rule.name().to_string(),
                severity: rule.severity(),
                category: rule.category(),
                recommendation: rule.recommendation().to_string(),
                default_profiles: Vec::new(),
            });

            if !entry
                .default_profiles
                .iter()
                .any(|name| name == profile_name)
            {
                entry.default_profiles.push(profile_name.to_string());
            }
        }
    }

    detail
}

fn profile_rules(profile: ScanProfile) -> Vec<Box<dyn AppStoreRule>> {
    match profile {
        ScanProfile::Basic => basic_rules(),
        ScanProfile::Full => full_rules(),
    }
}

fn basic_rules() -> Vec<Box<dyn AppStoreRule>> {
    vec![
        Box::new(MissingPrivacyManifestRule),
        Box::new(UsageDescriptionsRule),
        Box::new(UsageDescriptionsValueRule),
        Box::new(CameraUsageDescriptionRule),
        Box::new(AtsAuditRule),
        Box::new(AtsExceptionsGranularityRule),
        Box::new(EntitlementsMismatchRule),
        Box::new(EntitlementsProvisioningMismatchRule),
        Box::new(EmbeddedCodeSignatureTeamRule),
        Box::new(XcodeVersionRule),
        Box::new(BitcodeRule),
        Box::new(DeprecatedApiRule),
        Box::new(LaunchScreenStoryboardRule),
    ]
}

fn full_rules() -> Vec<Box<dyn AppStoreRule>> {
    vec![
        Box::new(MissingPrivacyManifestRule),
        Box::new(PrivacyManifestCompletenessRule),
        Box::new(PrivacyManifestSdkCrossCheckRule),
        Box::new(CameraUsageDescriptionRule),
        Box::new(UsageDescriptionsRule),
        Box::new(UsageDescriptionsValueRule),
        Box::new(InfoPlistRequiredKeysRule),
        Box::new(InfoPlistCapabilitiesRule),
        Box::new(LSApplicationQueriesSchemesAuditRule),
        Box::new(UIRequiredDeviceCapabilitiesAuditRule),
        Box::new(InfoPlistVersionConsistencyRule),
        Box::new(ExportComplianceRule),
        Box::new(AtsAuditRule),
        Box::new(AtsExceptionsGranularityRule),
        Box::new(EntitlementsMismatchRule),
        Box::new(EntitlementsProvisioningMismatchRule),
        Box::new(BundleMetadataConsistencyRule),
        Box::new(BundleResourceLeakageRule),
        Box::new(ExtensionEntitlementsCompatibilityRule),
        Box::new(PrivateApiRule),
        Box::new(EmbeddedCodeSignatureTeamRule),
        Box::new(XcodeVersionRule),
        Box::new(BitcodeRule),
        Box::new(DeprecatedApiRule),
        Box::new(LaunchScreenStoryboardRule),
        Box::new(AppIconAlphaRule),
        Box::new(BinaryStrippingRule),
        Box::new(OSVersionConsistencyRule),
    ]
}