Skip to main content

verifyos_cli/rules/
privacy_sdk.rs

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