diff --git a/crates/defguard_core/src/enterprise/posture/evaluation.rs b/crates/defguard_core/src/enterprise/posture/evaluation.rs index 22d3d16c01..202d673ac2 100644 --- a/crates/defguard_core/src/enterprise/posture/evaluation.rs +++ b/crates/defguard_core/src/enterprise/posture/evaluation.rs @@ -7,14 +7,7 @@ use sqlx::PgPool; use super::{ FailureReason, PostureCheckError, PostureResult, - version::{ - major_version_in_list, major_version_meets_minimum, minor_version_in_list, - version_meets_minimum, - }, - version_list::{ - ANDROID_OS_VERSIONS, CLIENT_VERSIONS, IOS_OS_VERSIONS, LINUX_KERNEL_VERSIONS, - MACOS_OS_VERSIONS, WINDOWS_OS_VERSIONS, - }, + version::{major_version_meets_minimum, version_meets_minimum}, }; use crate::enterprise::{ LicenseFeature, @@ -94,43 +87,22 @@ fn parse_os_type(s: &str) -> Option { /// OS and kernel version comparisons use major-only semantics: a device running /// the same major release as the policy minimum always passes regardless of /// minor or patch differences. Client version comparisons use full semver. -/// -/// For OS and kernel versions, the device-reported major must also appear in the -/// known version list for its OS type. An unrecognized version produces -/// [`FailureReason::UnrecognizedVersion`] regardless of the minimum comparison. fn evaluate_os_rule( rule: &DevicePostureOsRule, data: &DevicePostureData, failures: &mut Vec, ) { - let os_version_list: &[i32] = match rule.os_type { - OsType::Windows => WINDOWS_OS_VERSIONS, - OsType::Macos => MACOS_OS_VERSIONS, - OsType::Ios => IOS_OS_VERSIONS, - OsType::Android => ANDROID_OS_VERSIONS, - OsType::Linux => &[], // Linux uses kernel version list, not OS version list - }; - // min_os_version if let Some(required) = rule.min_os_version { match resolve_string_check(data.os_version.as_ref(), "os_version") { - Ok(Some(actual)) => match major_version_in_list(&actual, os_version_list) { + Ok(Some(actual)) => match major_version_meets_minimum(required, &actual) { + Some(true) => {} + Some(false) => { + failures.push(FailureReason::OsVersionTooOld { required, actual }); + } None => failures.push(FailureReason::CheckUnavailable { check: "os_version (unparseable)", }), - Some(false) => failures.push(FailureReason::UnrecognizedVersion { - check: "os_version", - actual, - }), - Some(true) => match major_version_meets_minimum(required, &actual) { - Some(true) => {} - Some(false) => { - failures.push(FailureReason::OsVersionTooOld { required, actual }); - } - None => failures.push(FailureReason::CheckUnavailable { - check: "os_version (unparseable)", - }), - }, }, Ok(None) => {} // NotApplicable — skip Err(name) => failures.push(FailureReason::CheckUnavailable { check: name }), @@ -189,23 +161,14 @@ fn evaluate_os_rule( // min_kernel_version (Linux only) if let Some(required) = rule.min_kernel_version { match resolve_string_check(data.linux_kernel_version.as_ref(), "linux_kernel_version") { - Ok(Some(actual)) => match major_version_in_list(&actual, LINUX_KERNEL_VERSIONS) { + Ok(Some(actual)) => match major_version_meets_minimum(required, &actual) { + Some(true) => {} + Some(false) => { + failures.push(FailureReason::KernelVersionTooOld { required, actual }); + } None => failures.push(FailureReason::CheckUnavailable { check: "linux_kernel_version (unparseable)", }), - Some(false) => failures.push(FailureReason::UnrecognizedVersion { - check: "linux_kernel_version", - actual, - }), - Some(true) => match major_version_meets_minimum(required, &actual) { - Some(true) => {} - Some(false) => { - failures.push(FailureReason::KernelVersionTooOld { required, actual }); - } - None => failures.push(FailureReason::CheckUnavailable { - check: "linux_kernel_version (unparseable)", - }), - }, }, Ok(None) => {} Err(name) => failures.push(FailureReason::CheckUnavailable { check: name }), @@ -305,28 +268,15 @@ pub(crate) async fn validate_posture( check: "defguard_client_version", }); } else { - match minor_version_in_list(actual, CLIENT_VERSIONS) { + match version_meets_minimum(required, actual, policy.allow_prerelease_client) { + Some(true) => {} + Some(false) => all_failures.push(FailureReason::ClientVersionTooOld { + required: required.to_owned(), + actual: actual.to_owned(), + }), None => all_failures.push(FailureReason::CheckUnavailable { check: "defguard_client_version (unparseable)", }), - Some(false) => all_failures.push(FailureReason::UnrecognizedVersion { - check: "client_version", - actual: actual.clone(), - }), - Some(true) => match version_meets_minimum( - required, - actual, - policy.allow_prerelease_client, - ) { - Some(true) => {} - Some(false) => all_failures.push(FailureReason::ClientVersionTooOld { - required: required.clone(), - actual: actual.clone(), - }), - None => all_failures.push(FailureReason::CheckUnavailable { - check: "defguard_client_version (unparseable)", - }), - }, } } } diff --git a/crates/defguard_core/src/enterprise/posture/mod.rs b/crates/defguard_core/src/enterprise/posture/mod.rs index bfcbaa6772..b3ee16ef03 100644 --- a/crates/defguard_core/src/enterprise/posture/mod.rs +++ b/crates/defguard_core/src/enterprise/posture/mod.rs @@ -52,11 +52,6 @@ pub enum FailureReason { CheckUnavailable { check: &'static str, }, - /// The device reported a version that is not in the known list for its OS or component. - UnrecognizedVersion { - check: &'static str, - actual: String, - }, } impl fmt::Display for FailureReason { @@ -105,9 +100,6 @@ impl fmt::Display for FailureReason { Self::CheckUnavailable { check } => { write!(f, "required check '{check}' could not be evaluated") } - Self::UnrecognizedVersion { check, actual } => { - write!(f, "unrecognized {check} version: {actual}") - } } } } diff --git a/crates/defguard_core/src/enterprise/posture/tests.rs b/crates/defguard_core/src/enterprise/posture/tests.rs index 1128cfafca..a763450124 100644 --- a/crates/defguard_core/src/enterprise/posture/tests.rs +++ b/crates/defguard_core/src/enterprise/posture/tests.rs @@ -103,15 +103,6 @@ fn linux_posture_data(os_version: &str, disk_encryption: bool) -> DevicePostureD } } -fn linux_posture_data_with_kernel(kernel_version: &str) -> DevicePostureData { - DevicePostureData { - defguard_client_version: "1.6.0".to_owned(), - os_type: "linux".to_owned(), - linux_kernel_version: Some(string_check_value(kernel_version)), - ..Default::default() - } -} - fn windows_posture_data() -> DevicePostureData { DevicePostureData { defguard_client_version: "1.6.0".to_owned(), @@ -402,69 +393,6 @@ async fn fail_missing_posture_data(_: PgPoolOptions, options: PgConnectOptions) )); } -/// Device reports OS version 99 (not in any known list) — must produce UnrecognizedVersion. -#[sqlx::test] -async fn fail_unrecognized_os_version(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - set_enterprise_license(); - let location_id = create_location(&pool).await; - - // Windows policy requiring min_os_version 11 — device claims version 99 (unknown). - let policy = DevicePosture { - id: defguard_common::db::NoId, - name: "win-unrecognized".to_owned(), - description: None, - min_client_version: None, - allow_prerelease_client: true, - } - .save(&pool) - .await - .unwrap(); - DevicePostureOsRule { - id: defguard_common::db::NoId, - posture_id: policy.id, - os_type: OsType::Windows, - min_os_version: Some(11), - disk_encryption_required: None, - antivirus_required: None, - ad_domain_joined_required: None, - windows_security_update_max_age: None, - min_kernel_version: None, - device_integrity_required: None, - android_security_patch_level_max_age: None, - } - .save(&pool) - .await - .unwrap(); - DevicePostureLocation::set_for_location( - &mut pool.acquire().await.unwrap(), - location_id, - &[policy.id], - ) - .await - .unwrap(); - - let data = DevicePostureData { - defguard_client_version: "1.6.0".to_owned(), - os_type: "windows".to_owned(), - os_version: Some(string_check_value("99.0")), - ..Default::default() - }; - - let result = validate_posture(&pool, &make_request(location_id, Some(data))) - .await - .unwrap(); - - assert!( - matches!( - result, - super::PostureResult::Fail(ref reasons) if reasons.len() == 1 - && matches!(reasons[0], super::FailureReason::UnrecognizedVersion { check: "os_version", .. }) - ), - "expected UnrecognizedVersion for Windows OS version 99" - ); -} - /// Device on a known-but-old OS version still produces OsVersionTooOld (regression guard). #[sqlx::test] async fn fail_os_version_too_old_regression(_: PgPoolOptions, options: PgConnectOptions) { @@ -528,89 +456,6 @@ async fn fail_os_version_too_old_regression(_: PgPoolOptions, options: PgConnect ); } -/// Device reports kernel version 99 (not in LINUX_KERNEL_VERSIONS) - must produce UnrecognizedVersion. -#[sqlx::test] -async fn fail_unrecognized_kernel_version(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - set_enterprise_license(); - let location_id = create_location(&pool).await; - - let policy = DevicePosture { - id: defguard_common::db::NoId, - name: "kernel-unrecognized".to_owned(), - description: None, - min_client_version: None, - allow_prerelease_client: true, - } - .save(&pool) - .await - .unwrap(); - DevicePostureOsRule { - id: defguard_common::db::NoId, - posture_id: policy.id, - os_type: OsType::Linux, - min_os_version: None, - disk_encryption_required: None, - antivirus_required: None, - ad_domain_joined_required: None, - windows_security_update_max_age: None, - min_kernel_version: Some(6), - device_integrity_required: None, - android_security_patch_level_max_age: None, - } - .save(&pool) - .await - .unwrap(); - DevicePostureLocation::set_for_location( - &mut pool.acquire().await.unwrap(), - location_id, - &[policy.id], - ) - .await - .unwrap(); - - let data = linux_posture_data_with_kernel("99.0.0"); - - let result = validate_posture(&pool, &make_request(location_id, Some(data))) - .await - .unwrap(); - - assert!( - matches!( - result, - super::PostureResult::Fail(ref reasons) if reasons.len() == 1 - && matches!(reasons[0], super::FailureReason::UnrecognizedVersion { check: "linux_kernel_version", .. }) - ), - "expected UnrecognizedVersion for kernel version 99" - ); -} - -/// Client reports version 1.7.0 (major.minor "1.7" not in CLIENT_VERSIONS) — UnrecognizedVersion. -#[sqlx::test] -async fn fail_unrecognized_client_version(_: PgPoolOptions, options: PgConnectOptions) { - let pool = setup_pool(options).await; - set_enterprise_license(); - let location_id = create_location(&pool).await; - - save_linux_policy(&pool, location_id, None, Some("1.6"), true).await; - - let mut data = linux_posture_data("6.1.0", true); - data.defguard_client_version = "1.7.0".to_owned(); - - let result = validate_posture(&pool, &make_request(location_id, Some(data))) - .await - .unwrap(); - - assert!( - matches!( - result, - super::PostureResult::Fail(ref reasons) if reasons.len() == 1 - && matches!(reasons[0], super::FailureReason::UnrecognizedVersion { check: "client_version", .. }) - ), - "expected UnrecognizedVersion for client 1.7.0" - ); -} - /// Client on known version 1.6.x that meets the minimum still passes. #[sqlx::test] async fn pass_known_client_version_meets_minimum(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_core/src/enterprise/posture/version.rs b/crates/defguard_core/src/enterprise/posture/version.rs index 9f3849351a..086963e4ee 100644 --- a/crates/defguard_core/src/enterprise/posture/version.rs +++ b/crates/defguard_core/src/enterprise/posture/version.rs @@ -58,25 +58,6 @@ pub(super) fn major_version_meets_minimum(required: i32, actual: &str) -> Option Some(act.major >= required as u64) } -/// Returns `Some(true)` when `actual`'s major component is present in `allowed`, -/// `Some(false)` when it is not, and `None` when `actual` cannot be parsed. -pub(super) fn major_version_in_list(actual: &str, allowed: &[i32]) -> Option { - let act = parse_version_lenient(actual)?; - Some(allowed.contains(&(act.major as i32))) -} - -/// Returns `Some(true)` when `actual`'s `"major.minor"` string matches any entry -/// in `allowed`, `Some(false)` when it does not, and `None` when `actual` cannot -/// be parsed. -/// -/// This is used for client version validation: the list stores `"major.minor"` -/// strings (e.g. `"1.6"`) and the client reports a full semver (e.g. `"1.6.3"`). -pub(super) fn minor_version_in_list(actual: &str, allowed: &[&str]) -> Option { - let act = parse_version_lenient(actual)?; - let key = format!("{}.{}", act.major, act.minor); - Some(allowed.contains(&key.as_str())) -} - #[cfg(test)] mod unit_tests { use super::*; @@ -127,34 +108,6 @@ mod unit_tests { assert_eq!(major_version_meets_minimum(22, "not-a-version"), None); } - #[test] - fn major_version_in_list_checks_membership() { - let list = &[10_i32, 11, 14, 15]; - // Major is in the list. - assert_eq!(major_version_in_list("14.5.1", list), Some(true)); - assert_eq!(major_version_in_list("10", list), Some(true)); - // Major is not in the list. - assert_eq!(major_version_in_list("12.0.0", list), Some(false)); - assert_eq!(major_version_in_list("99.1", list), Some(false)); - // Unparseable — None. - assert_eq!(major_version_in_list("not-a-version", list), None); - } - - #[test] - fn minor_version_in_list_checks_membership() { - let list = &["1.6", "2.0"]; - // Exact major.minor match. - assert_eq!(minor_version_in_list("1.6.3", list), Some(true)); - assert_eq!(minor_version_in_list("2.0.0", list), Some(true)); - // Different minor — not in list. - assert_eq!(minor_version_in_list("1.7.0", list), Some(false)); - assert_eq!(minor_version_in_list("1.5.9", list), Some(false)); - // Completely unknown major. - assert_eq!(minor_version_in_list("3.0.0", list), Some(false)); - // Unparseable — None. - assert_eq!(minor_version_in_list("not-a-version", list), None); - } - #[test] fn parse_version_lenient_handles_short_forms() { assert!(parse_version_lenient("11").is_some()); diff --git a/crates/defguard_core/src/enterprise/posture/version_list.rs b/crates/defguard_core/src/enterprise/posture/version_list.rs index 752eb71f34..714e244f11 100644 --- a/crates/defguard_core/src/enterprise/posture/version_list.rs +++ b/crates/defguard_core/src/enterprise/posture/version_list.rs @@ -3,9 +3,9 @@ pub const CLIENT_VERSIONS: &[&str] = &["2.1"]; /// Minimum OS versions available for posture checks. pub const WINDOWS_OS_VERSIONS: &[i32] = &[10, 11]; -pub const MACOS_OS_VERSIONS: &[i32] = &[13, 14, 15, 26]; -pub const IOS_OS_VERSIONS: &[i32] = &[17, 18, 26]; -pub const ANDROID_OS_VERSIONS: &[i32] = &[13, 14, 15, 16]; +pub const MACOS_OS_VERSIONS: &[i32] = &[13, 14, 15, 26, 27]; +pub const IOS_OS_VERSIONS: &[i32] = &[17, 18, 26, 27]; +pub const ANDROID_OS_VERSIONS: &[i32] = &[13, 14, 15, 16, 17]; /// Valid Linux kernel major versions for posture rules. pub const LINUX_KERNEL_VERSIONS: &[i32] = &[5, 6, 7]; diff --git a/flake.lock b/flake.lock index 6f4f415ee1..cdddb4ff2d 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1781577229, - "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=", + "lastModified": 1783224372, + "narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f", + "rev": "d407951447dcd00442e97087bf374aad70c04cea", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1781752752, - "narHash": "sha256-kVG5tV9hddPviGAgqf9sGSuStvv+HAB9onfKqGptV0k=", + "lastModified": 1783404876, + "narHash": "sha256-DAh1CfiRVwr8Szkj5PiHmsqh10NHPb8SKPx7w/B+l9E=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c06d86dabe5b92982b9d67acccb9990d58da3a0e", + "rev": "3c161f20193bd91a73913b417e5b06d41860336d", "type": "github" }, "original": {