verifyos_cli/rules/
privacy_sdk.rs1use 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}