Skip to main content

verifyos_cli/rules/
ats.rs

1use crate::rules::core::{
2    AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4
5pub struct AtsAuditRule;
6
7impl AppStoreRule for AtsAuditRule {
8    fn id(&self) -> &'static str {
9        "RULE_ATS_AUDIT"
10    }
11
12    fn name(&self) -> &'static str {
13        "ATS Exceptions Detected"
14    }
15
16    fn category(&self) -> RuleCategory {
17        RuleCategory::Ats
18    }
19
20    fn severity(&self) -> Severity {
21        Severity::Warning
22    }
23
24    fn recommendation(&self) -> &'static str {
25        "Remove ATS exceptions or scope them to specific domains with justification."
26    }
27
28    fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
29        let Some(plist) = artifact.info_plist else {
30            return Ok(RuleReport {
31                status: RuleStatus::Skip,
32                message: Some("Info.plist not found".to_string()),
33                evidence: None,
34            });
35        };
36
37        let Some(ats_dict) = plist.get_dictionary("NSAppTransportSecurity") else {
38            return Ok(RuleReport {
39                status: RuleStatus::Pass,
40                message: None,
41                evidence: None,
42            });
43        };
44
45        let mut issues = Vec::new();
46
47        if let Some(true) = ats_dict
48            .get("NSAllowsArbitraryLoads")
49            .and_then(|v| v.as_boolean())
50        {
51            issues.push("NSAllowsArbitraryLoads=true".to_string());
52        }
53
54        if let Some(true) = ats_dict
55            .get("NSAllowsArbitraryLoadsInWebContent")
56            .and_then(|v| v.as_boolean())
57        {
58            issues.push("NSAllowsArbitraryLoadsInWebContent=true".to_string());
59        }
60
61        if let Some(domains) = ats_dict
62            .get("NSExceptionDomains")
63            .and_then(|v| v.as_dictionary())
64        {
65            for (domain, config) in domains {
66                if let Some(true) = config
67                    .as_dictionary()
68                    .and_then(|d| d.get("NSExceptionAllowsInsecureHTTPLoads"))
69                    .and_then(|v| v.as_boolean())
70                {
71                    issues.push(format!("NSExceptionAllowsInsecureHTTPLoads for {domain}"));
72                }
73            }
74        }
75
76        if issues.is_empty() {
77            return Ok(RuleReport {
78                status: RuleStatus::Pass,
79                message: None,
80                evidence: None,
81            });
82        }
83
84        Ok(RuleReport {
85            status: RuleStatus::Fail,
86            message: Some("ATS exceptions detected".to_string()),
87            evidence: Some(issues.join("; ")),
88        })
89    }
90}