verifyos_cli/rules/
os_version.rs1use crate::rules::core::{
2 AppStoreRule, ArtifactContext, RuleCategory, RuleError, RuleReport, RuleStatus, Severity,
3};
4
5pub struct OSVersionConsistencyRule;
6
7impl AppStoreRule for OSVersionConsistencyRule {
8 fn id(&self) -> &'static str {
9 "RULE_OS_VERSION_CONSISTENCY"
10 }
11
12 fn name(&self) -> &'static str {
13 "Minimum OS Version Consistency Check"
14 }
15
16 fn category(&self) -> RuleCategory {
17 RuleCategory::Bundling
18 }
19
20 fn severity(&self) -> Severity {
21 Severity::Warning
22 }
23
24 fn recommendation(&self) -> &'static str {
25 "Ensure MinimumOSVersion is consistent across the main app and all extensions. Extensions should generally not have a higher MinimumOSVersion than the main app."
26 }
27
28 fn evaluate(&self, artifact: &ArtifactContext) -> Result<RuleReport, RuleError> {
29 let Some(main_plist) = artifact.info_plist else {
30 return Ok(RuleReport {
31 status: RuleStatus::Skip,
32 message: Some("Main Info.plist not found".to_string()),
33 evidence: None,
34 });
35 };
36
37 let main_version = main_plist.get_string("MinimumOSVersion").unwrap_or("0.0");
38 let mut issues = Vec::new();
39
40 if let Ok(nested) = artifact.nested_bundles() {
41 for bundle in nested {
42 let bundle_name = &bundle.display_name;
45 if let Ok(Some(sub_plist)) = artifact.bundle_info_plist(&bundle.bundle_path) {
46 if let Some(sub_version) = sub_plist.get_string("MinimumOSVersion") {
47 if is_version_higher(sub_version, main_version) {
48 issues.push(format!(
49 "{} has higher MinimumOSVersion ({}) than main app ({})",
50 bundle_name, sub_version, main_version
51 ));
52 }
53 }
54 }
55 }
56 }
57
58 if issues.is_empty() {
59 return Ok(RuleReport {
60 status: RuleStatus::Pass,
61 message: Some(format!(
62 "MinimumOSVersion ({}) is consistent across all bundles",
63 main_version
64 )),
65 evidence: None,
66 });
67 }
68
69 Ok(RuleReport {
70 status: RuleStatus::Fail,
71 message: Some("Inconsistent MinimumOSVersion detected".to_string()),
72 evidence: Some(issues.join(" | ")),
73 })
74 }
75}
76
77fn is_version_higher(v1: &str, v2: &str) -> bool {
78 let p1: Vec<u32> = v1
79 .split('.')
80 .filter_map(|s| s.parse::<u32>().ok())
81 .collect();
82 let p2: Vec<u32> = v2
83 .split('.')
84 .filter_map(|s| s.parse::<u32>().ok())
85 .collect();
86
87 for i in 0..std::cmp::max(p1.len(), p2.len()) {
88 let n1 = *p1.get(i).unwrap_or(&0);
89 let n2 = *p2.get(i).unwrap_or(&0);
90 if n1 > n2 {
91 return true;
92 }
93 if n1 < n2 {
94 return false;
95 }
96 }
97 false
98}