Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 17 additions & 67 deletions crates/defguard_core/src/enterprise/posture/evaluation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -94,43 +87,22 @@ fn parse_os_type(s: &str) -> Option<OsType> {
/// 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<defguard_common::db::Id>,
data: &DevicePostureData,
failures: &mut Vec<FailureReason>,
) {
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 }),
Expand Down Expand Up @@ -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 }),
Expand Down Expand Up @@ -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)",
}),
},
}
}
}
Expand Down
8 changes: 0 additions & 8 deletions crates/defguard_core/src/enterprise/posture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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}")
}
}
}
}
Expand Down
155 changes: 0 additions & 155 deletions crates/defguard_core/src/enterprise/posture/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
47 changes: 0 additions & 47 deletions crates/defguard_core/src/enterprise/posture/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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<bool> {
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::*;
Expand Down Expand Up @@ -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());
Expand Down
Loading
Loading