Skip to main content

verifyos_cli/rules/
privacy_sdk.rs

1use crate::parsers::macho_scanner::scan_sdks_from_app_bundle;
2use crate::parsers::plist_reader::InfoPlist;
3use crate::rules::core::{
4    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
5};
6
7pub struct PrivacyManifestSdkCrossCheckRule;
8
9impl AppStoreRule for PrivacyManifestSdkCrossCheckRule {
10    fn id(&self) -> &'static str {
11        "RULE_PRIVACY_SDK_CROSSCHECK"
12    }
13
14    fn name(&self) -> &'static str {
15        "Privacy Manifest vs SDK Usage"
16    }
17
18    fn category(&self) -> RuleCategory {
19        RuleCategory::Privacy
20    }
21
22    fn severity(&self) -> Severity {
23        Severity::Warning
24    }
25
26    fn recommendation(&self) -> &'static str {
27        "Ensure PrivacyInfo.xcprivacy declares data collection and accessed APIs for included SDKs."
28    }
29
30    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
31        let manifest_path = artifact.app_bundle_path.join("PrivacyInfo.xcprivacy");
32        if !manifest_path.exists() {
33            return Ok(RuleReport {
34                status: RuleStatus::Skip,
35                message: Some("PrivacyInfo.xcprivacy not found".to_string()),
36                evidence: None,
37            });
38        }
39
40        let manifest = InfoPlist::from_file(&manifest_path)
41            .map_err(|_| crate::rules::entitlements::EntitlementsError::ParseFailure)?;
42
43        let scan = match scan_sdks_from_app_bundle(artifact.app_bundle_path) {
44            Ok(scan) => scan,
45            Err(err) => {
46                return Ok(RuleReport {
47                    status: RuleStatus::Skip,
48                    message: Some(format!("SDK scan skipped: {err}")),
49                    evidence: None,
50                });
51            }
52        };
53
54        if scan.hits.is_empty() {
55            return Ok(RuleReport {
56                status: RuleStatus::Pass,
57                message: Some("No SDK signatures detected".to_string()),
58                evidence: None,
59            });
60        }
61
62        let has_data_types = manifest
63            .get_value("NSPrivacyCollectedDataTypes")
64            .and_then(|v| v.as_array())
65            .map(|arr| !arr.is_empty())
66            .unwrap_or(false);
67        let has_accessed_api_types = manifest
68            .get_value("NSPrivacyAccessedAPITypes")
69            .and_then(|v| v.as_array())
70            .map(|arr| !arr.is_empty())
71            .unwrap_or(false);
72
73        if has_data_types || has_accessed_api_types {
74            return Ok(RuleReport {
75                status: RuleStatus::Pass,
76                message: Some("Privacy manifest includes SDK data declarations".to_string()),
77                evidence: None,
78            });
79        }
80
81        Ok(RuleReport {
82            status: RuleStatus::Fail,
83            message: Some("SDKs detected but privacy manifest lacks declarations".to_string()),
84            evidence: Some(format!("SDK signatures: {}", scan.hits.join(", "))),
85        })
86    }
87}