Skip to main content

verifyos_cli/rules/
privacy_manifest.rs

1use crate::parsers::plist_reader::InfoPlist;
2use crate::rules::core::{
3    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
4};
5
6pub struct PrivacyManifestCompletenessRule;
7
8impl AppStoreRule for PrivacyManifestCompletenessRule {
9    fn id(&self) -> &'static str {
10        "RULE_PRIVACY_MANIFEST_COMPLETENESS"
11    }
12
13    fn name(&self) -> &'static str {
14        "Privacy Manifest Completeness"
15    }
16
17    fn category(&self) -> RuleCategory {
18        RuleCategory::Privacy
19    }
20
21    fn severity(&self) -> Severity {
22        Severity::Warning
23    }
24
25    fn recommendation(&self) -> &'static str {
26        "Declare accessed API categories in PrivacyInfo.xcprivacy."
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("PrivacyInfo.xcprivacy is empty or invalid; skipping".to_string()),
44                    evidence: Some(manifest_path.display().to_string()),
45                });
46            }
47        };
48
49        let scan = match artifact.usage_scan() {
50            Ok(scan) => scan,
51            Err(err) => {
52                return Ok(RuleReport {
53                    status: RuleStatus::Skip,
54                    message: Some(format!("Usage scan skipped: {err}")),
55                    evidence: None,
56                });
57            }
58        };
59
60        if scan.required_keys.is_empty() && !scan.requires_location_key {
61            return Ok(RuleReport {
62                status: RuleStatus::Pass,
63                message: Some("No usage APIs detected".to_string()),
64                evidence: None,
65            });
66        }
67
68        let has_accessed_api_types = manifest
69            .get_value("NSPrivacyAccessedAPITypes")
70            .and_then(|v| v.as_array())
71            .map(|arr| !arr.is_empty())
72            .unwrap_or(false);
73
74        if has_accessed_api_types {
75            return Ok(RuleReport {
76                status: RuleStatus::Pass,
77                message: None,
78                evidence: None,
79            });
80        }
81
82        Ok(RuleReport {
83            status: RuleStatus::Fail,
84            message: Some("Privacy manifest missing accessed API types".to_string()),
85            evidence: Some("NSPrivacyAccessedAPITypes is missing or empty".to_string()),
86        })
87    }
88}