verifyos_cli/rules/
privacy_sdk.rs1use 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("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.sdk_scan() {
50 Ok(scan) => scan,
51 Err(err) => {
52 return Ok(RuleReport {
53 status: RuleStatus::Skip,
54 message: Some(format!("SDK scan skipped: {err}")),
55 evidence: None,
56 });
57 }
58 };
59
60 if scan.hits.is_empty() {
61 return Ok(RuleReport {
62 status: RuleStatus::Pass,
63 message: Some("No SDK signatures detected".to_string()),
64 evidence: None,
65 });
66 }
67
68 let has_data_types = manifest
69 .get_value("NSPrivacyCollectedDataTypes")
70 .and_then(|v| v.as_array())
71 .map(|arr| !arr.is_empty())
72 .unwrap_or(false);
73 let has_accessed_api_types = manifest
74 .get_value("NSPrivacyAccessedAPITypes")
75 .and_then(|v| v.as_array())
76 .map(|arr| !arr.is_empty())
77 .unwrap_or(false);
78
79 if has_data_types || has_accessed_api_types {
80 return Ok(RuleReport {
81 status: RuleStatus::Pass,
82 message: Some("Privacy manifest includes SDK data declarations".to_string()),
83 evidence: None,
84 });
85 }
86
87 Ok(RuleReport {
88 status: RuleStatus::Fail,
89 message: Some("SDKs detected but privacy manifest lacks declarations".to_string()),
90 evidence: Some(format!("SDK signatures: {}", scan.hits.join(", "))),
91 })
92 }
93}