diff --git a/biome.json b/biome.json index 0e1c1731..4bc6b7c6 100644 --- a/biome.json +++ b/biome.json @@ -27,9 +27,7 @@ "bracketSpacing": true, "expand": "auto", "useEditorconfig": true, - "includes": [ - "./src/**" - ] + "includes": ["./src/**"] }, "linter": { "enabled": true, @@ -70,9 +68,7 @@ "noArrayIndexKey": "off" } }, - "includes": [ - "src/**" - ] + "includes": ["src/**"] }, "javascript": { "formatter": { @@ -94,9 +90,7 @@ }, "overrides": [ { - "includes": [ - "**/*.js" - ] + "includes": ["**/*.js"] } ], "assist": { diff --git a/src-tauri/.sqlx/query-9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056.json b/src-tauri/.sqlx/query-50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1.json similarity index 84% rename from src-tauri/.sqlx/query-9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056.json rename to src-tauri/.sqlx/query-50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1.json index 012a54b3..7692367f 100644 --- a/src-tauri/.sqlx/query-9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056.json +++ b/src-tauri/.sqlx/query-50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE instance_id = $1 AND service_location_mode <= $2 ORDER BY name ASC", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n posture_check_required FROM location WHERE instance_id = $1 AND service_location_mode <= $2 ORDER BY name ASC", "describe": { "columns": [ { @@ -67,6 +67,11 @@ "name": "service_location_mode: ServiceLocationMode", "ordinal": 12, "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 13, + "type_info": "Bool" } ], "parameters": { @@ -85,8 +90,9 @@ false, false, false, + false, false ] }, - "hash": "9137d3329ed718f211b5654af41b297c31706f5a5ad9ac400be116db7113a056" + "hash": "50543a38f8f09cd45b16bf8b93d2ff42252aa0833f57c8ffa98e3471b9e663a1" } diff --git a/src-tauri/.sqlx/query-ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f.json b/src-tauri/.sqlx/query-7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c.json similarity index 56% rename from src-tauri/.sqlx/query-ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f.json rename to src-tauri/.sqlx/query-7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c.json index a05f49a0..7eb7d1e7 100644 --- a/src-tauri/.sqlx/query-ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f.json +++ b/src-tauri/.sqlx/query-7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, service_location_mode) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id \"id!\"", + "query": "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, service_location_mode, posture_check_required) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id \"id!\"", "describe": { "columns": [ { @@ -10,11 +10,11 @@ } ], "parameters": { - "Right": 12 + "Right": 13 }, "nullable": [ true ] }, - "hash": "ea39145f2cdc783bc78b32363cce32a87bd603debccaec23b160150766bdcd9f" + "hash": "7b9f3c02d868a7da19beed6aad5ee22962668200b1ffb1fb7df967be5af0c50c" } diff --git a/src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json b/src-tauri/.sqlx/query-858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3.json similarity index 77% rename from src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json rename to src-tauri/.sqlx/query-858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3.json index e023a3a7..c6135a03 100644 --- a/src-tauri/.sqlx/query-d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e.json +++ b/src-tauri/.sqlx/query-858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE service_location_mode <= $1 ORDER BY name ASC;", + "query": "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\", posture_check_required FROM location WHERE service_location_mode <= $1 ORDER BY name ASC;", "describe": { "columns": [ { @@ -67,6 +67,11 @@ "name": "service_location_mode: ServiceLocationMode", "ordinal": 12, "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 13, + "type_info": "Bool" } ], "parameters": { @@ -85,8 +90,9 @@ false, false, false, + false, false ] }, - "hash": "d8d908979a8573ee2c32828fa562d1bf171b4a6e224cc238680e2e856811c62e" + "hash": "858556d40a6fc015f2664045a00837ec18e3c0fd94b6ded74a60ac6edc57a5b3" } diff --git a/src-tauri/.sqlx/query-76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175.json b/src-tauri/.sqlx/query-865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b.json similarity index 86% rename from src-tauri/.sqlx/query-76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175.json rename to src-tauri/.sqlx/query-865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b.json index 9c6f65ed..ddf2ceb9 100644 --- a/src-tauri/.sqlx/query-76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175.json +++ b/src-tauri/.sqlx/query-865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE id = $1", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n posture_check_required FROM location WHERE id = $1", "describe": { "columns": [ { @@ -67,6 +67,11 @@ "name": "service_location_mode: ServiceLocationMode", "ordinal": 12, "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 13, + "type_info": "Bool" } ], "parameters": { @@ -85,8 +90,9 @@ false, false, false, + false, false ] }, - "hash": "76c5c9b75df39afca9cd07530ab0569d3d6f9d8924458c8b357dd400966f4175" + "hash": "865203f8f64866f1895aede956dfd8f373772e3c406f4482e6a4022a796cd46b" } diff --git a/src-tauri/.sqlx/query-85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6.json b/src-tauri/.sqlx/query-ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0.json similarity index 86% rename from src-tauri/.sqlx/query-85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6.json rename to src-tauri/.sqlx/query-ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0.json index 1615e9b4..dd6d4011 100644 --- a/src-tauri/.sqlx/query-85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6.json +++ b/src-tauri/.sqlx/query-ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM location WHERE pubkey = $1;", + "query": "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\",\n posture_check_required FROM location WHERE pubkey = $1;", "describe": { "columns": [ { @@ -67,6 +67,11 @@ "name": "service_location_mode: ServiceLocationMode", "ordinal": 12, "type_info": "Integer" + }, + { + "name": "posture_check_required", + "ordinal": 13, + "type_info": "Bool" } ], "parameters": { @@ -85,8 +90,9 @@ false, false, false, + false, false ] }, - "hash": "85f8edf373d3bf1d405a8fed804d9d04839e69a6c2c5cb8ad5c2f8e19547a2f6" + "hash": "ec008998cc09e79017a3edd82550df0afd4bd8488391475908272d9cf7c6dbd0" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a04ea4fa..000f4803 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1439,7 +1439,7 @@ dependencies = [ "sqlx", "struct-patch", "strum", - "swift-rs", + "sysinfo", "tauri", "tauri-build", "tauri-plugin-clipboard-manager", @@ -1472,6 +1472,7 @@ dependencies = [ "windows-acl", "windows-service", "windows-sys 0.61.2", + "wmi", "x25519-dalek", ] @@ -2132,6 +2133,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -2218,6 +2234,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -3760,6 +3777,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -4009,6 +4035,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "objc2-io-surface" version = "0.3.2" @@ -4122,9 +4158,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.4" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "dunce", "is-wsl", @@ -6097,18 +6133,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "struct-patch" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d4caaaccd69c9b56c5f5b33d4dca462464d3275230e4d2d3739ba6d4bf5bcb" +checksum = "b6647f17706475257679fb9a4a71d85b222633d398d685010d7cc2f4c99de375" dependencies = [ "struct-patch-derive", ] [[package]] name = "struct-patch-derive" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1671c6f0992b1b4cb4f5f8ea4a58f9a5f7f895a7638ef9690633dcec0aa67944" +checksum = "203b436d4379c929ff3e7340b89c3a57ca3be8a76b3376448176575e8ca3f258" dependencies = [ "proc-macro2", "quote", @@ -6210,6 +6246,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4deba334e1190ba7cb498327affa11e5ece10d26a30ab2f27fcf09504b8d8b6" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.62.2", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -8957,6 +9007,20 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3a159352..e9f252f2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,7 +30,7 @@ authors = ["Defguard"] edition = "2021" homepage = "https://github.com/DefGuard/client" license-file = "../LICENSE.md" -rust-version = "1.87" +rust-version = "1.95" version = "1.6.9" [package] @@ -84,8 +84,9 @@ sqlx = { version = "0.8", features = [ "uuid", "macros", ] } -struct-patch = "0.10" +struct-patch = "0.11" strum = { version = "0.28", features = ["derive"] } +sysinfo = { version = "0.39", default-features = false, features = ["apple-app-store", "system"] } tauri = { version = "2", features = [ "native-tls-vendored", "image-png", @@ -128,9 +129,6 @@ objc2 = "0.6" objc2-foundation = "0.3" objc2-network-extension = "0.3" -[target.'cfg(target_os = "macos")'.build-dependencies] -swift-rs = { version = "1.0", features = ["build"] } - [target.'cfg(unix)'.dependencies] nix = { version = "0.31", features = ["user", "fs"] } tokio-stream = "0.1" @@ -164,6 +162,7 @@ windows-sys = { version = "0.61", features = [ # Network address change notifications (NotifyAddrChange) "Win32_NetworkManagement_IpHelper", ] } +wmi = {version = "0.18", default-features = false} [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 91455f0e..e38786b2 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -17,8 +17,13 @@ fn main() -> Result<(), Box> { // Make all messages serde-serializable. .type_attribute(".", "#[derive(serde::Serialize,serde::Deserialize)]") .compile_protos( - &["proto/client/client.proto", "proto/core/proxy.proto"], - &["proto/client", "proto/core"], + &[ + "proto/v1/client/client.proto", + "proto/v1/core/proxy.proto", + "proto/enterprise/v2/posture/posture.proto", + "proto/common/client_types.proto", + ], + &["proto"], )?; tauri_build::build(); diff --git a/src-tauri/cli/build.rs b/src-tauri/cli/build.rs index fb4ca05e..a25227de 100644 --- a/src-tauri/cli/build.rs +++ b/src-tauri/cli/build.rs @@ -10,7 +10,7 @@ fn main() -> Result<(), Box> { ) // Make all messages serde-serializable. .type_attribute(".", "#[derive(serde::Deserialize,serde::Serialize)]") - .compile_protos(&["../proto/core/proxy.proto"], &["../proto/core"])?; + .compile_protos(&["../proto/v1/core/proxy.proto"], &["../proto"])?; Ok(()) } diff --git a/src-tauri/cli/src/bin/dg.rs b/src-tauri/cli/src/bin/dg.rs index 13c20b7e..d3edc4ce 100644 --- a/src-tauri/cli/src/bin/dg.rs +++ b/src-tauri/cli/src/bin/dg.rs @@ -19,6 +19,10 @@ use defguard_wireguard_rs::{ error::WireguardInterfaceError, key::Key, net::IpAddrMask, peer::Peer, InterfaceConfiguration, WGApi, WireguardInterfaceApi, }; +use proto::defguard::client_types::{ + Device, DeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, + InstanceInfo, InstanceInfoRequest, InstanceInfoResponse, NewDevice, +}; use reqwest::{Client, StatusCode, Url}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -31,15 +35,36 @@ use tracing::{debug, error, info, level_filters::LevelFilter, trace, warn}; use tracing_subscriber::EnvFilter; mod proto { - include!(concat!(env!("OUT_DIR"), "/defguard.proxy.rs")); + pub mod defguard { + pub mod client_types { + include!(concat!(env!("OUT_DIR"), "/defguard.client_types.rs")); + } + + pub mod enterprise { + pub mod posture { + pub mod v2 { + include!(concat!( + env!("OUT_DIR"), + "/defguard.enterprise.posture.v2.rs" + )); + } + } + } + + pub mod proxy { + pub mod v1 { + include!(concat!(env!("OUT_DIR"), "/defguard.proxy.v1.rs")); + } + } + } } #[derive(Clone, Default, Deserialize, Serialize)] struct CliConfig { private_key: Key, - device: proto::Device, - device_config: proto::DeviceConfig, - instance_info: proto::InstanceInfo, + device: Device, + device_config: DeviceConfig, + instance_info: InstanceInfo, // polling token used for further client-core communication token: Option, } @@ -283,11 +308,11 @@ async fn enroll(base_url: &Url, token: String) -> Result { url.set_path("/api/v1/enrollment/start"); let result = client .post(url) - .json(&proto::EnrollmentStartRequest { token }) + .json(&EnrollmentStartRequest { token }) .send() .await?; - let response: proto::EnrollmentStartResponse = if result.status() == StatusCode::OK { + let response: EnrollmentStartResponse = if result.status() == StatusCode::OK { let result = result.json().await?; debug!( "Enrollment start request has been successfully sent to Defguard Proxy. Received a \ @@ -314,7 +339,7 @@ async fn enroll(base_url: &Url, token: String) -> Result { url.set_path("/api/v1/enrollment/create_device"); let result = client .post(url) - .json(&proto::NewDevice { + .json(&NewDevice { // The name is ignored by the server as it's set by the user before the enrollment. name: String::new(), pubkey: pubkey.to_string(), @@ -323,7 +348,7 @@ async fn enroll(base_url: &Url, token: String) -> Result { .send() .await?; - let response: proto::DeviceConfigResponse = if result.status() == StatusCode::OK { + let response: DeviceConfigResponse = if result.status() == StatusCode::OK { let result = result.json().await?; debug!( "The device public key has been successfully sent to Defguard Proxy. The device should \ @@ -367,19 +392,15 @@ const INTERVAL_SECONDS: Duration = Duration::from_secs(30); const HTTP_REQ_TIMEOUT: Duration = Duration::from_secs(5); /// Fetch configuration from Defguard proxy. -async fn fetch_config( - client: &Client, - url: Url, - token: String, -) -> Result { +async fn fetch_config(client: &Client, url: Url, token: String) -> Result { let result = client .post(url.clone()) - .json(&proto::InstanceInfoRequest { token }) + .json(&InstanceInfoRequest { token }) .timeout(HTTP_REQ_TIMEOUT) .send() .await?; - let instance_response: proto::InstanceInfoResponse = if result.status() == StatusCode::OK { + let instance_response: InstanceInfoResponse = if result.status() == StatusCode::OK { result.json().await? } else if result.status() == StatusCode::PAYMENT_REQUIRED { return Err(CliError::EnterpriseDisabled); diff --git a/src-tauri/migrations/20260511093103_posture_check_required.sql b/src-tauri/migrations/20260511093103_posture_check_required.sql new file mode 100644 index 00000000..c9743138 --- /dev/null +++ b/src-tauri/migrations/20260511093103_posture_check_required.sql @@ -0,0 +1 @@ +ALTER TABLE location ADD COLUMN posture_check_required BOOLEAN NOT NULL DEFAULT false; diff --git a/src-tauri/proto b/src-tauri/proto index 5dfc8c8d..84a2bf71 160000 --- a/src-tauri/proto +++ b/src-tauri/proto @@ -1 +1 @@ -Subproject commit 5dfc8c8d23ac0613108a2b7b921fd9a97613bb3a +Subproject commit 84a2bf71b1435d956cae2f7933ab9b7e880292b6 diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1fc1c55f..7b4e00b8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -36,7 +36,7 @@ use crate::{ global_log_watcher::{spawn_global_log_watcher_task, stop_global_log_watcher_task}, service_log_watcher::stop_log_watcher_task, }, - proto::DeviceConfigResponse, + proto::defguard::client_types::DeviceConfigResponse, tray::{configure_tray_icon, reload_tray_menu}, utils::{ construct_platform_header, disconnect_interface, get_location_interface_details, @@ -242,7 +242,7 @@ pub async fn save_device_config( let instance_info = response .instance .expect("Missing instance info in device config response"); - let mut instance: Instance = instance_info.into(); + let mut instance = Instance::from(instance_info); if response.token.is_some() { debug!( "The newly saved device config has a polling token, automatic configuration polling \ @@ -250,7 +250,7 @@ pub async fn save_device_config( ); } else { warn!( - "Missing polling token for instance {}, core and/or proxy services may need an update, \ + "Missing polling token for instance {}, Core and/or Edge services may need an update, \ configuration polling won't work", instance.name, ); @@ -539,22 +539,21 @@ pub(crate) async fn locations_changed( instance: &Instance, device_config: &DeviceConfigResponse, ) -> Result { - let db_locations: HashSet> = - Location::find_by_instance_id(transaction.as_mut(), instance.id, true) - .await? - .into_iter() - .map(|location| { - let mut new_location = Location::::from(location); - // Ignore `route_all_traffic` flag as Defguard core does not have it. - new_location.route_all_traffic = false; - new_location - }) - .collect(); - let core_locations: HashSet = device_config + let db_locations = Location::find_by_instance_id(transaction.as_mut(), instance.id, true) + .await? + .into_iter() + .map(|location| { + let mut new_location = Location::::from(location); + // Ignore `route_all_traffic` flag as Defguard core does not have it. + new_location.route_all_traffic = false; + new_location + }) + .collect::>(); + let core_locations = device_config .configs .iter() .map(|config| config.clone().into_location(instance.id)) - .collect(); + .collect::>(); Ok(db_locations != core_locations) } diff --git a/src-tauri/src/database/models/instance.rs b/src-tauri/src/database/models/instance.rs index 0227afce..9779e149 100644 --- a/src-tauri/src/database/models/instance.rs +++ b/src-tauri/src/database/models/instance.rs @@ -26,8 +26,8 @@ impl fmt::Display for Instance { } } -impl From for Instance { - fn from(instance_info: proto::InstanceInfo) -> Self { +impl From for Instance { + fn from(instance_info: proto::defguard::client_types::InstanceInfo) -> Self { let client_traffic_policy = ClientTrafficPolicy::from(&instance_info); Self { id: NoId, @@ -138,8 +138,8 @@ impl Instance { } // This compares proto::InstanceInfo, not to be confused with regular InstanceInfo defined below -impl PartialEq for Instance { - fn eq(&self, other: &proto::InstanceInfo) -> bool { +impl PartialEq for Instance { + fn eq(&self, other: &proto::defguard::client_types::InstanceInfo) -> bool { let other_policy = ClientTrafficPolicy::from(other); self.name == other.name && self.uuid == other.id @@ -223,8 +223,8 @@ pub enum ClientTrafficPolicy { } /// Retrieves `ClientTrafficPolicy` from `proto::InstanceInfo` while ensuring backwards compatibility -impl From<&proto::InstanceInfo> for ClientTrafficPolicy { - fn from(instance: &proto::InstanceInfo) -> Self { +impl From<&proto::defguard::client_types::InstanceInfo> for ClientTrafficPolicy { + fn from(instance: &proto::defguard::client_types::InstanceInfo) -> Self { match ( instance.client_traffic_policy, #[allow(deprecated)] diff --git a/src-tauri/src/database/models/location.rs b/src-tauri/src/database/models/location.rs index 7381bc29..54829cc6 100644 --- a/src-tauri/src/database/models/location.rs +++ b/src-tauri/src/database/models/location.rs @@ -17,7 +17,7 @@ use crate::{ }; use crate::{ error::Error, - proto::{ + proto::defguard::client_types::{ LocationMfaMode as ProtoLocationMfaMode, ServiceLocationMode as ProtoServiceLocationMode, }, }; @@ -80,6 +80,8 @@ pub struct Location { pub keepalive_interval: i64, pub location_mfa_mode: LocationMfaMode, pub service_location_mode: ServiceLocationMode, + #[serde(default)] + pub posture_check_required: bool, } impl fmt::Display for Location { @@ -107,14 +109,16 @@ impl Location { let max_service_location_mode = Self::get_service_location_mode_filter(include_service_locations); query_as!( - Self, - "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, network_id,\ - route_all_traffic, keepalive_interval, \ - location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ + Self, + "SELECT id, instance_id, name, address, pubkey, endpoint, allowed_ips, dns, \ + network_id, route_all_traffic, keepalive_interval, \ + location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\", \ + posture_check_required \ FROM location WHERE service_location_mode <= $1 \ ORDER BY name ASC;", max_service_location_mode - ) + ) .fetch_all(executor) .await } @@ -127,7 +131,8 @@ impl Location { query!( "UPDATE location SET instance_id = $1, name = $2, address = $3, pubkey = $4, \ endpoint = $5, allowed_ips = $6, dns = $7, network_id = $8, route_all_traffic = $9, \ - keepalive_interval = $10, location_mfa_mode = $11, service_location_mode = $12 WHERE id = $13", + keepalive_interval = $10, location_mfa_mode = $11, service_location_mode = $12 \ + WHERE id = $13", self.instance_id, self.name, self.address, @@ -159,7 +164,9 @@ impl Location { Self, "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, \ network_id, route_all_traffic, keepalive_interval, \ - location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ + location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\", + posture_check_required \ FROM location WHERE id = $1", location_id ) @@ -180,7 +187,10 @@ impl Location { query_as!( Self, "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, \ - network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ + network_id, route_all_traffic, keepalive_interval, \ + location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\", + posture_check_required \ FROM location WHERE instance_id = $1 AND service_location_mode <= $2 \ ORDER BY name ASC", instance_id, @@ -200,7 +210,10 @@ impl Location { query_as!( Self, "SELECT id \"id: _\", instance_id, name, address, pubkey, endpoint, allowed_ips, dns, \ - network_id, route_all_traffic, keepalive_interval, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" \ + network_id, route_all_traffic, keepalive_interval, \ + location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\", + posture_check_required \ FROM location WHERE pubkey = $1;", pubkey ) @@ -345,8 +358,9 @@ impl Location { Ok(interface_config) } - /// Returns a filter value that can be used in SQL queries like `service_location_mode <= ?` when querying locations - /// to exclude (<= 1) or include service locations (all service locations modes). + /// Returns a filter value that can be used in SQL queries like `service_location_mode <= ?` + /// when querying locations to exclude (<= 1) or include service locations (all service + /// locations modes). fn get_service_location_mode_filter(include_service_locations: bool) -> i32 { if include_service_locations { i32::MAX @@ -364,8 +378,9 @@ impl Location { // Insert a new record when there is no ID let id = query_scalar!( "INSERT INTO location (instance_id, name, address, pubkey, endpoint, allowed_ips, \ - dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, service_location_mode) \ - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) \ + dns, network_id, route_all_traffic, keepalive_interval, location_mfa_mode, \ + service_location_mode, posture_check_required) \ + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) \ RETURNING id \"id!\"", self.instance_id, self.name, @@ -379,6 +394,7 @@ impl Location { self.keepalive_interval, self.location_mfa_mode, self.service_location_mode, + self.posture_check_required, ) .fetch_one(executor) .await?; @@ -397,6 +413,7 @@ impl Location { keepalive_interval: self.keepalive_interval, location_mfa_mode: self.location_mfa_mode, service_location_mode: self.service_location_mode, + posture_check_required: self.posture_check_required, }) } } @@ -424,6 +441,7 @@ impl From> for Location { keepalive_interval: location.keepalive_interval, location_mfa_mode: location.location_mfa_mode, service_location_mode: location.service_location_mode, + posture_check_required: location.posture_check_required, } } } diff --git a/src-tauri/src/enterprise/inspector/linux.rs b/src-tauri/src/enterprise/inspector/linux.rs new file mode 100644 index 00000000..bce09a52 --- /dev/null +++ b/src-tauri/src/enterprise/inspector/linux.rs @@ -0,0 +1,69 @@ +use std::process::Command; + +use serde::Deserialize; + +use super::UnavailableReason; + +#[derive(Deserialize)] +struct LsblkDevice { + fstype: Option, + #[serde(rename = "type")] + device_type: Option, + children: Option>, +} + +impl LsblkDevice { + // Check if device is encrypted. + #[must_use] + fn is_crypto(&self) -> bool { + if let Some(fstype) = &self.fstype { + fstype == "crypto_LUKS" + } else if let Some(device_type) = &self.device_type { + device_type == "crypt" + } else { + false + } + } +} + +#[derive(Deserialize)] +struct LsblkOutput { + blockdevices: Vec, +} + +/// Determine if any block device has "crypto_LUKS" type. +fn check_luks() -> Result { + let output = Command::new("lsblk") + .args(["-Jo", "NAME,FSTYPE,TYPE"]) + .output() + .map_err(|_| UnavailableReason::DetectionFailed)?; + if !output.status.success() { + return Err(UnavailableReason::DetectionFailed); + } + + let output: LsblkOutput = + serde_json::from_slice(&output.stdout).map_err(|_| UnavailableReason::DetectionFailed)?; + + // Check for LUKS. + for device in output.blockdevices { + if device.is_crypto() { + return Ok(true); + } + if let Some(children) = &device.children { + for child in children { + if child.is_crypto() { + return Ok(true); + } + } + } + } + + Ok(false) +} + +// https://labex.io/tutorials/linux-how-to-check-if-disk-encryption-is-enabled-in-linux-558786 +// FIXME: This will check all available disks, so if any is encrypted, it will succeed. +pub(super) fn disk_encryption_status() -> Result { + // TODO: `zfs list -jo name,encryption,mountpoint` and check for `/` and `on`. + check_luks() +} diff --git a/src-tauri/src/enterprise/inspector/macos.rs b/src-tauri/src/enterprise/inspector/macos.rs new file mode 100644 index 00000000..779aaaf2 --- /dev/null +++ b/src-tauri/src/enterprise/inspector/macos.rs @@ -0,0 +1,25 @@ +use std::process::Command; + +use super::UnavailableReason; + +/// Check if FileVault has been enabled. +pub(super) fn disk_encryption_status() -> Result { + let output = Command::new("fdesetup") + .arg("isactive") + .output() + .map_err(|_| UnavailableReason::DetectionFailed)?; + let stdout = String::from_utf8_lossy(&output.stdout); + + Ok(stdout.trim_end() == "true") +} + +/// Check if System Integrity Protection has been enabled. +pub(super) fn system_integrity_status() -> Result { + let output = Command::new("csrutil") + .arg("status") + .output() + .map_err(|_| UnavailableReason::DetectionFailed)?; + let stdout = String::from_utf8_lossy(&output.stdout); + + Ok(stdout.trim_end() == "System Integrity Protection status: enabled.") +} diff --git a/src-tauri/src/enterprise/inspector/mod.rs b/src-tauri/src/enterprise/inspector/mod.rs new file mode 100644 index 00000000..424a2144 --- /dev/null +++ b/src-tauri/src/enterprise/inspector/mod.rs @@ -0,0 +1,221 @@ +#[cfg(target_os = "linux")] +pub(crate) mod linux; +#[cfg(target_os = "macos")] +pub(crate) mod macos; +#[cfg(test)] +mod tests; +#[cfg(windows)] +pub(crate) mod windows; + +use std::{error::Error, fmt}; + +use sysinfo::System; + +use crate::{ + proto::defguard::enterprise::posture::v2::{ + bool_check, string_check, BoolCheck, DevicePostureData, StringCheck, UnavailableReason, + }, + VERSION, +}; + +impl fmt::Display for UnavailableReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Unspecified => f.write_str("nspecified"), + Self::DetectionFailed => f.write_str("detection failed"), + Self::NotApplicable => f.write_str("not applicable on this platform"), + Self::InsufficientPermissions => f.write_str("insufficient permissions"), + } + } +} + +impl Error for UnavailableReason {} + +/// Operating system type. +pub enum OsType { + FreeBSD, + Linux, + MacOS, + NetBSD, + Windows, +} + +impl fmt::Display for OsType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::FreeBSD => f.write_str("FreeBSD"), + Self::Linux => f.write_str("Linux"), + Self::MacOS => f.write_str("macOS"), + Self::NetBSD => f.write_str("NetBSD"), + Self::Windows => f.write_str("Windows"), + } + } +} + +impl OsType { + /// Returns the OS type of the current machine. + /// Note: Unsupported machines won't compile. + #[must_use] + pub fn this_machine() -> Self { + #[cfg(target_os = "macos")] + { + Self::MacOS + } + #[cfg(target_os = "freebsd")] + { + Self::FreeBSD + } + #[cfg(target_os = "linux")] + { + Self::Linux + } + #[cfg(target_os = "netbsd")] + { + Self::NetBSD + } + #[cfg(windows)] + { + Self::Windows + } + } +} + +/// Returns the operating system type. +#[must_use] +fn os_type() -> OsType { + OsType::this_machine() +} + +/// Returns the operating system name. +fn os_name() -> Result { + System::name().ok_or(UnavailableReason::DetectionFailed) +} + +/// Returns the operating system version. +fn os_version() -> Result { + System::os_version().ok_or(UnavailableReason::DetectionFailed) +} + +/// Returns the Linux kernel version. +fn linux_kernel_version() -> Result { + #[cfg(target_os = "linux")] + { + System::kernel_version().ok_or(UnavailableReason::DetectionFailed) + } + + #[cfg(not(target_os = "linux"))] + { + Err(UnavailableReason::NotApplicable) + } +} + +/// Returns the disk encryption status, preferably for the system volume. +fn disk_encryption_status() -> Result { + #[cfg(target_os = "macos")] + { + macos::disk_encryption_status() + } + + #[cfg(windows)] + { + windows::disk_encryption_status() + } + + #[cfg(target_os = "linux")] + { + linux::disk_encryption_status() + } +} + +/// Returns the antivirus status. +fn anti_virus_status() -> Result { + #[cfg(windows)] + { + windows::anti_virus_status() + } + + #[cfg(not(windows))] + { + Err(UnavailableReason::NotApplicable) + } +} + +/// Checks whether the computer is part of a domain. +fn part_of_domain() -> Result { + #[cfg(windows)] + { + windows::part_of_domain() + } + + #[cfg(not(windows))] + { + Err(UnavailableReason::NotApplicable) + } +} + +/// Returns the device integrity status. +fn device_integrity() -> Result { + #[cfg(target_os = "macos")] + { + macos::system_integrity_status() + } + + #[cfg(not(target_os = "macos"))] + Err(UnavailableReason::NotApplicable) +} + +/// Returns the security update status. +fn security_update_status() -> Result { + #[cfg(windows)] + { + windows::security_update_status() + } + + #[cfg(not(windows))] + { + Err(UnavailableReason::NotApplicable) + } +} + +/// Convert `Result` to `BoolCheck`. +impl From> for BoolCheck { + fn from(value: Result) -> Self { + Self { + result: Some(match value { + Ok(inner) => bool_check::Result::Value(inner), + Err(err) => bool_check::Result::Unavailable(err as i32), + }), + } + } +} + +/// Convert `Result` to `StringCheck`. +impl From> for StringCheck { + fn from(value: Result) -> Self { + Self { + result: Some(match value { + Ok(inner) => string_check::Result::Value(inner), + Err(err) => string_check::Result::Unavailable(err as i32), + }), + } + } +} + +impl DevicePostureData { + /// Performs system inspection and returns the results. + #[must_use] + pub fn new() -> Self { + Self { + defguard_client_version: VERSION.to_owned(), + os_type: os_type().to_string(), + os_name: Some(StringCheck::from(os_name())), + os_version: Some(StringCheck::from(os_version())), + disk_encryption: Some(BoolCheck::from(disk_encryption_status())), + antivirus_present: Some(BoolCheck::from(anti_virus_status())), + windows_ad_domain_joined: Some(BoolCheck::from(part_of_domain())), + windows_security_update_current: Some(BoolCheck::from(security_update_status())), + linux_kernel_version: Some(StringCheck::from(linux_kernel_version())), + device_integrity: Some(BoolCheck::from(device_integrity())), + } + } +} diff --git a/src-tauri/src/enterprise/inspector/tests/linux.rs b/src-tauri/src/enterprise/inspector/tests/linux.rs new file mode 100644 index 00000000..c437993a --- /dev/null +++ b/src-tauri/src/enterprise/inspector/tests/linux.rs @@ -0,0 +1,22 @@ +use super::super::{disk_encryption_status, os_name, os_type, os_version, OsType}; + +#[test] +fn test_os_type() { + assert!(matches!(os_type(), OsType::Linux)); +} + +#[test] +fn test_os_name() { + assert!(os_name().unwrap().ends_with("Linux")); +} + +#[test] +fn test_os_version() { + assert!(os_version().is_ok()); +} + +#[test] +#[ignore = "development machine only"] +fn test_disk_encryption() { + assert!(!disk_encryption_status().unwrap()); +} diff --git a/src-tauri/src/enterprise/inspector/tests/macos.rs b/src-tauri/src/enterprise/inspector/tests/macos.rs new file mode 100644 index 00000000..c2f628a5 --- /dev/null +++ b/src-tauri/src/enterprise/inspector/tests/macos.rs @@ -0,0 +1,30 @@ +use super::super::{ + device_integrity, disk_encryption_status, os_name, os_type, os_version, OsType, +}; + +#[test] +fn test_os_type() { + assert!(matches!(os_type(), OsType::MacOS)); +} + +#[test] +fn test_os_name() { + assert_eq!(os_name().unwrap(), "Darwin"); +} + +#[test] +fn test_os_version() { + assert!(os_version().is_ok()); +} + +#[test] +#[ignore = "development machine only"] +fn test_disk_encryption() { + assert!(disk_encryption_status().unwrap()); +} + +#[test] +#[ignore = "development machine only"] +fn test_device_integrity() { + assert!(device_integrity().unwrap()); +} diff --git a/src-tauri/src/enterprise/inspector/tests/mod.rs b/src-tauri/src/enterprise/inspector/tests/mod.rs new file mode 100644 index 00000000..0337daa1 --- /dev/null +++ b/src-tauri/src/enterprise/inspector/tests/mod.rs @@ -0,0 +1,6 @@ +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(windows)] +mod windows; diff --git a/src-tauri/src/enterprise/inspector/tests/windows.rs b/src-tauri/src/enterprise/inspector/tests/windows.rs new file mode 100644 index 00000000..a9a11805 --- /dev/null +++ b/src-tauri/src/enterprise/inspector/tests/windows.rs @@ -0,0 +1,43 @@ +use super::super::{ + anti_virus_status, disk_encryption_status, os_name, os_type, os_version, part_of_domain, + security_update_status, OsType, +}; + +#[test] +fn test_os_type() { + assert!(matches!(os_type(), OsType::Windows)); +} + +#[test] +fn test_os_name() { + assert_eq!(os_name().unwrap(), "Windows"); +} + +#[test] +fn test_os_version() { + assert!(os_version().is_ok()); +} + +#[test] +#[ignore = "development machine only"] +fn test_disk_encryption() { + assert!(!disk_encryption_status().unwrap()); +} + +#[test] +#[ignore = "development machine only"] +fn test_anti_virus() { + assert!(anti_virus_status().unwrap()); +} + +#[test] +#[ignore = "development machine only"] +fn test_part_of_domain() { + assert!(!part_of_domain().unwrap()); +} + +#[test] +#[ignore = "development machine only"] +fn test_security_update_status() { + assert!(security_update_status().unwrap()); +} diff --git a/src-tauri/src/enterprise/inspector/windows.rs b/src-tauri/src/enterprise/inspector/windows.rs new file mode 100644 index 00000000..ff86a671 --- /dev/null +++ b/src-tauri/src/enterprise/inspector/windows.rs @@ -0,0 +1,169 @@ +// TODO: use `async_raw_query` + +use serde::Deserialize; +use time::{Date, OffsetDateTime}; + +use wmi::{AuthLevel, WMIConnection, WMIError}; + +use super::UnavailableReason; + +const MAX_QUICKFIX_DAYS: i64 = 60; + +#[derive(Deserialize)] +#[serde(rename = "Win32_EncryptableVolume")] +#[serde(rename_all = "PascalCase")] +struct Win32EncryptableVolume { + // drive_letter: Option, + // 0 = unprotected, 1 = protected, 2 = unknown + protection_status: u32, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AntiVirusProduct { + // display_name: String, + product_state: u32, +} + +#[derive(Deserialize)] +#[serde(rename = "Win32_ComputerSystem")] +#[serde(rename_all = "PascalCase")] +struct Win32ComputerSystem { + // domain: String, + part_of_domain: bool, +} + +#[derive(Deserialize)] +#[serde(rename = "Win32_OperatingSystem")] +#[serde(rename_all = "PascalCase")] +struct Win32OperatingSystem { + system_drive: String, +} + +// Custom format for `installed_on`. +time::serde::format_description!( + wmidate, + Date, + "[month padding:none]/[day padding:none]/[year]" +); + +#[derive(Deserialize)] +#[serde(rename = "Win32_QuickFixEngineering")] +#[serde(rename_all = "PascalCase")] +struct Win32QuickFixEngineering { + //hot_fix_id: String, + #[serde(with = "wmidate::option", default)] + installed_on: Option, + //description: Option, // "Update" or "Security Update" +} + +/// Convert `WMIError` to `UnavailableReason`. +impl From for UnavailableReason { + fn from(err: WMIError) -> Self { + if let WMIError::HResultError { .. } = err { + UnavailableReason::InsufficientPermissions + } else { + UnavailableReason::DetectionFailed + } + } +} + +/// Determine system drive letter. +fn system_drive_letter() -> Result { + let conn = WMIConnection::new()?; + let mut results: Vec = conn.query()?; + match results.pop() { + Some(result) => Ok(result.system_drive), + None => Err(UnavailableReason::DetectionFailed), + } +} + +/// This requires Administrator access, and only detects BitLocker for drive C:. +/// +/// Equivalent to PowerShell command: +/// `Get-WmiObject -Namespace "root\CIMV2\Security\MicrosoftVolumeEncryption" -query "SELECT * FROM Win32_EncryptableVolume"` +pub(super) fn disk_encryption_status() -> Result { + let drive_letter = system_drive_letter()?; + + let conn = + WMIConnection::with_namespace_path("root\\CIMV2\\Security\\MicrosoftVolumeEncryption")?; + conn.set_proxy_blanket(AuthLevel::PktPrivacy)?; + + let volumes: Vec = conn.raw_query(format!( + "SELECT ProtectionStatus FROM Win32_EncryptableVolume WHERE DriveLetter='{drive_letter}'" + ))?; + + // XXX: query all drives and .filter()? + + match volumes.first() { + Some(vol) => { + return match vol.protection_status { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(UnavailableReason::DetectionFailed), + }; + } + None => Err(UnavailableReason::DetectionFailed), + } +} + +/// Determine AntiVirus status. +/// +/// Check manually in PowerShell: +/// `Get-CimInstance -Namespace root\SecurityCenter2 -ClassName AntivirusProduct` +/// +/// Equivalent to PowerShell command: +/// `Get-WmiObject -Namespace "root\SecurityCenter2" -query "SELECT * FROM AntiVirusProduct"` +pub(super) fn anti_virus_status() -> Result { + let conn = WMIConnection::with_namespace_path("root\\SecurityCenter2")?; + let products: Vec = conn.query()?; + for product in products { + let enabled = (product.product_state & 0x0001_0000) != 0; + let realtime = (product.product_state & 0x0002_0000) != 0; + // let up_to_date = (product.product_state & 0x0004_0000) != 0; + if enabled || realtime { + return Ok(true); + } + } + + Ok(false) +} + +/// Check if this machine is part of an Active Directory domain. +/// +/// Check manually in PowerShell: +/// `Get-CimInstance -ClassName Win32_ComputerSystem` +/// +/// Equivalent to PowerShell command: +/// `Get-WmiObject -query "SELECT * FROM Win32_ComputerSystem"` +pub(super) fn part_of_domain() -> Result { + let conn = WMIConnection::new()?; + let system = conn.get::()?; + Ok(system.part_of_domain) +} + +/// Find the latest security patch. +/// +/// Check manually in PowerShell: +/// `Get-CimInstance -ClassName Win32_QuickFixEngineering` +/// +/// Equivalent to PowerShell command: +/// `Get-WmiObject -query "SELECT * FROM Win32_QuickFixEngineering"` +pub(super) fn security_update_status() -> Result { + let conn = WMIConnection::new()?; + let fixes: Vec = conn.query().unwrap(); + + // Days from today + let today = OffsetDateTime::now_utc().date(); + let mut max_days = i64::MAX; + for fix in fixes { + if let Some(installed_on) = fix.installed_on { + let days = (today - installed_on).whole_days(); + if days < max_days { + max_days = days; + } + } + } + + Ok(max_days <= MAX_QUICKFIX_DAYS) +} diff --git a/src-tauri/src/enterprise/mod.rs b/src-tauri/src/enterprise/mod.rs index 8e1f8e8a..6074c054 100644 --- a/src-tauri/src/enterprise/mod.rs +++ b/src-tauri/src/enterprise/mod.rs @@ -1,3 +1,4 @@ +pub mod inspector; pub mod models; pub mod periodic; pub mod provisioning; diff --git a/src-tauri/src/enterprise/periodic/config.rs b/src-tauri/src/enterprise/periodic/config.rs index 6153c822..7aca2494 100644 --- a/src-tauri/src/enterprise/periodic/config.rs +++ b/src-tauri/src/enterprise/periodic/config.rs @@ -21,7 +21,9 @@ use crate::{ }, error::Error, events::EventKey, - proto::{DeviceConfigResponse, InstanceInfoRequest, InstanceInfoResponse}, + proto::defguard::client_types::{ + DeviceConfigResponse, InstanceInfoRequest, InstanceInfoResponse, + }, utils::construct_platform_header, CLIENT_PLATFORM_HEADER, CLIENT_VERSION_HEADER, MIN_CORE_VERSION, MIN_PROXY_VERSION, PKG_VERSION, diff --git a/src-tauri/src/log_watcher/global_log_watcher.rs b/src-tauri/src/log_watcher/global_log_watcher.rs index 74ed2b82..86a92d71 100644 --- a/src-tauri/src/log_watcher/global_log_watcher.rs +++ b/src-tauri/src/log_watcher/global_log_watcher.rs @@ -637,7 +637,7 @@ pub async fn spawn_global_log_watcher_task( let app_state = handle.state::(); // Show logs only from the last hour - let from = Some(Utc::now() - Duration::from_secs(60 * 60)); + let from = Some(Utc::now() - Duration::from_hours(1)); let event_topic = "log-update-global".to_string(); diff --git a/src-tauri/src/periodic/purge_stats.rs b/src-tauri/src/periodic/purge_stats.rs index 81fafd06..c8ca2494 100644 --- a/src-tauri/src/periodic/purge_stats.rs +++ b/src-tauri/src/periodic/purge_stats.rs @@ -7,8 +7,7 @@ use crate::database::{ DB_POOL, }; -// 12 hours -const PURGE_INTERVAL: Duration = Duration::from_secs(12 * 60 * 60); +const PURGE_INTERVAL: Duration = Duration::from_hours(12); /// Periodically purges location and tunnel stats. /// diff --git a/src-tauri/src/periodic/version.rs b/src-tauri/src/periodic/version.rs index 9b8b9c10..b58c5c13 100644 --- a/src-tauri/src/periodic/version.rs +++ b/src-tauri/src/periodic/version.rs @@ -5,7 +5,7 @@ use tokio::time::interval; use crate::{appstate::AppState, commands::get_latest_app_version, events::EventKey}; -const INTERVAL_IN_SECONDS: Duration = Duration::from_secs(12 * 60 * 60); // 12 hours +const INTERVAL_IN_SECONDS: Duration = Duration::from_hours(12); pub async fn poll_version(app_handle: AppHandle) { debug!("Starting the latest application version polling loop."); diff --git a/src-tauri/src/proto.rs b/src-tauri/src/proto.rs index ad041709..bbc7341d 100644 --- a/src-tauri/src/proto.rs +++ b/src-tauri/src/proto.rs @@ -3,9 +3,27 @@ use crate::database::models::{ Id, NoId, }; -tonic::include_proto!("defguard.proxy"); +pub(crate) mod defguard { + pub(crate) mod enterprise { + pub(crate) mod posture { + pub(crate) mod v2 { + tonic::include_proto!("defguard.enterprise.posture.v2"); + } + } + } + + pub(crate) mod proxy { + pub(crate) mod v1 { + tonic::include_proto!("defguard.proxy.v1"); + } + } + + pub mod client_types { + tonic::include_proto!("defguard.client_types"); + } +} -impl DeviceConfig { +impl defguard::client_types::DeviceConfig { #[must_use] pub(crate) fn into_location(self, instance_id: Id) -> Location { let location_mfa_mode = match self.location_mfa_mode { @@ -41,6 +59,7 @@ impl DeviceConfig { keepalive_interval: self.keepalive_interval.into(), location_mfa_mode, service_location_mode, + posture_check_required: self.posture_check_required.unwrap_or_default(), } } } diff --git a/src-tauri/src/service/mod.rs b/src-tauri/src/service/mod.rs index 4d617432..9ba7bf30 100644 --- a/src-tauri/src/service/mod.rs +++ b/src-tauri/src/service/mod.rs @@ -2,7 +2,7 @@ pub mod client; pub mod config; pub mod proto { - tonic::include_proto!("client"); + tonic::include_proto!("defguard.client.v1"); } #[cfg(not(target_os = "macos"))] pub mod daemon; diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 94ff99c7..db6fcca1 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -271,19 +271,13 @@ async fn handle_location_tray_menu(id: String, app: &AppHandle) { info!("Connect location with ID {id}"); // Check if MFA is enabled. If so, trigger modal on frontend. if location.mfa_enabled() { - info!( - "MFA enabled for location with ID {:?}, trigger MFA modal", - location.id - ); + info!("MFA enabled for location with ID {id}, trigger MFA modal"); show_main_window(app); let _ = app.emit(EventKey::MfaTrigger.into(), &location); } else if let Err(err) = connect(location_id, ConnectionType::Location, None, app.clone()).await { - info!( - "Unable to connect location with ID {}, error: {err:?}", - location.id - ); + info!("Unable to connect location with ID {id}, error: {err:?}"); } } } diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 7e0eb05c..3018e8ca 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -43,7 +43,7 @@ use crate::{ error::Error, events::EventKey, log_watcher::service_log_watcher::spawn_log_watcher_task, - proto::ClientPlatformInfo, + proto::defguard::client_types::ClientPlatformInfo, ConnectionType, }; #[cfg(not(target_os = "macos"))] diff --git a/src/pages/client/components/MfaModalProvider.tsx b/src/pages/client/components/MfaModalProvider.tsx index 680100c5..50447b69 100644 --- a/src/pages/client/components/MfaModalProvider.tsx +++ b/src/pages/client/components/MfaModalProvider.tsx @@ -3,7 +3,7 @@ import { type PropsWithChildren, useEffect } from 'react'; import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; import { MFAModal } from '../pages/ClientInstancePage/components/LocationsList/modals/MFAModal/MFAModal'; import { useMFAModal } from '../pages/ClientInstancePage/components/LocationsList/modals/MFAModal/useMFAModal'; -import type { CommonWireguardFields } from '../types'; +import { type CommonWireguardFields, TauriEventKey } from '../types'; type Props = PropsWithChildren; @@ -13,17 +13,20 @@ type Payload = { export const MfaModalProvider = ({ children }: Props) => { const openMFAModal = useMFAModal((state) => state.open); - // listen for rust backend requesting MFA + // listen for Rust backend requesting MFA useEffect(() => { let unlisten: UnlistenFn; (async () => { - unlisten = await listen('mfa-trigger', ({ payload: { location } }) => { - if (isPresent(location)) { - openMFAModal(location); - } - }); + unlisten = await listen( + TauriEventKey.MFA_TRIGGER, + ({ payload: { location } }) => { + if (isPresent(location)) { + openMFAModal(location); + } + }, + ); })(); return () => { diff --git a/src/pages/client/types.ts b/src/pages/client/types.ts index 3434187b..8c0b1e66 100644 --- a/src/pages/client/types.ts +++ b/src/pages/client/types.ts @@ -75,6 +75,7 @@ export type CommonWireguardFields = { pubkey: string; instance_id: number; network_id: number; + // TODO: device posture data, if available }; export type SelectedInstance = {