verifyos_cli/rules/
privacy_manifest.rs1use 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(
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.usage_scan() {
52 Ok(scan) => scan,
53 Err(err) => {
54 return Ok(RuleReport {
55 status: RuleStatus::Skip,
56 message: Some(format!("Usage scan skipped: {err}")),
57 evidence: None,
58 });
59 }
60 };
61
62 if scan.required_keys.is_empty() && !scan.requires_location_key {
63 return Ok(RuleReport {
64 status: RuleStatus::Pass,
65 message: Some("No usage APIs detected".to_string()),
66 evidence: None,
67 });
68 }
69
70 let declared_types: std::collections::HashSet<String> = manifest
71 .get_value("NSPrivacyAccessedAPITypes")
72 .and_then(|v| v.as_array())
73 .map(|arr| {
74 arr.iter()
75 .filter_map(|v| {
76 v.as_dictionary()
77 .and_then(|d| d.get("NSPrivacyAccessedAPIType"))
78 .and_then(|t| t.as_string())
79 .map(|s| s.to_string())
80 })
81 .collect()
82 })
83 .unwrap_or_default();
84
85 let mut missing_categories = Vec::new();
86 for cat in &scan.privacy_categories {
87 if !declared_types.contains(*cat) {
88 missing_categories.push(*cat);
89 }
90 }
91
92 if missing_categories.is_empty() {
93 return Ok(RuleReport {
94 status: RuleStatus::Pass,
95 message: None,
96 evidence: None,
97 });
98 }
99
100 missing_categories.sort();
101 Ok(RuleReport {
102 status: RuleStatus::Fail,
103 message: Some("Privacy manifest missing required API declarations".to_string()),
104 evidence: Some(format!(
105 "Missing categories in NSPrivacyAccessedAPITypes: {}",
106 missing_categories.join(", ")
107 )),
108 })
109 }
110}