Add USB storage device control feature

- Add USB policy fields to machines schema (policy, status, error)
- Create usbPolicyEvents table for audit logging
- Implement Convex mutations/queries for USB policy management
- Add REST API endpoints for desktop agent communication
- Create Rust usb_control module for Windows registry manipulation
- Integrate USB policy check in agent heartbeat loop
- Add USB policy control component in admin device overview
- Add localhost:3001 to auth trustedOrigins for dev

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-04 13:30:59 -03:00
parent 0e9310d6e4
commit 49aa143a80
11 changed files with 1116 additions and 1 deletions

View file

@ -69,6 +69,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"sha2",
"sysinfo",
"tauri",
"tauri-build",
@ -1094,6 +1095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -3291,6 +3293,7 @@ checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",

View file

@ -36,3 +36,6 @@ parking_lot = "0.12"
hostname = "0.4"
base64 = "0.22"
sha2 = "0.10"
[target.'cfg(windows)'.dependencies]
winreg = "0.55"

View file

@ -1129,6 +1129,109 @@ async fn post_heartbeat(
Ok(())
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct UsbPolicyResponse {
pending: bool,
policy: Option<String>,
applied_at: Option<i64>,
}
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct UsbPolicyStatusReport {
machine_token: String,
status: String,
error: Option<String>,
current_policy: Option<String>,
}
async fn check_and_apply_usb_policy(base_url: &str, token: &str) {
let url = format!("{}/api/machines/usb-policy?machineToken={}", base_url, token);
let response = match HTTP_CLIENT.get(&url).send().await {
Ok(resp) => resp,
Err(e) => {
eprintln!("[agent] Falha ao verificar politica USB: {e}");
return;
}
};
let policy_response: UsbPolicyResponse = match response.json().await {
Ok(data) => data,
Err(e) => {
eprintln!("[agent] Falha ao parsear resposta de politica USB: {e}");
return;
}
};
if !policy_response.pending {
return;
}
let policy_str = match policy_response.policy {
Some(p) => p,
None => {
eprintln!("[agent] Politica USB pendente mas sem valor de policy");
return;
}
};
eprintln!("[agent] Aplicando politica USB: {}", policy_str);
#[cfg(target_os = "windows")]
{
use crate::usb_control::{apply_usb_policy, UsbPolicy};
let policy = match UsbPolicy::from_str(&policy_str) {
Some(p) => p,
None => {
eprintln!("[agent] Politica USB invalida: {}", policy_str);
report_usb_policy_status(base_url, token, "FAILED", Some(format!("Politica invalida: {}", policy_str)), None).await;
return;
}
};
match apply_usb_policy(policy) {
Ok(result) => {
eprintln!("[agent] Politica USB aplicada com sucesso: {:?}", result);
report_usb_policy_status(base_url, token, "APPLIED", None, Some(policy_str)).await;
}
Err(e) => {
eprintln!("[agent] Falha ao aplicar politica USB: {e}");
report_usb_policy_status(base_url, token, "FAILED", Some(e.to_string()), None).await;
}
}
}
#[cfg(not(target_os = "windows"))]
{
eprintln!("[agent] Controle de USB nao suportado neste sistema operacional");
report_usb_policy_status(base_url, token, "FAILED", Some("Sistema operacional nao suportado".to_string()), None).await;
}
}
async fn report_usb_policy_status(
base_url: &str,
token: &str,
status: &str,
error: Option<String>,
current_policy: Option<String>,
) {
let url = format!("{}/api/machines/usb-policy", base_url);
let report = UsbPolicyStatusReport {
machine_token: token.to_string(),
status: status.to_string(),
error,
current_policy,
};
if let Err(e) = HTTP_CLIENT.post(&url).json(&report).send().await {
eprintln!("[agent] Falha ao reportar status de politica USB: {e}");
}
}
struct HeartbeatHandle {
token: String,
base_url: String,
@ -1198,6 +1301,9 @@ impl AgentRuntime {
eprintln!("[agent] Falha inicial ao enviar heartbeat: {error}");
}
// Verifica politica USB apos heartbeat inicial
check_and_apply_usb_policy(&base_clone, &token_clone).await;
let mut ticker = tokio::time::interval(Duration::from_secs(interval));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
@ -1215,6 +1321,9 @@ impl AgentRuntime {
{
eprintln!("[agent] Falha ao enviar heartbeat: {error}");
}
// Verifica politica USB apos cada heartbeat
check_and_apply_usb_policy(&base_clone, &token_clone).await;
}
});

View file

@ -1,8 +1,10 @@
mod agent;
#[cfg(target_os = "windows")]
mod rustdesk;
mod usb_control;
use agent::{collect_inventory_plain, collect_profile, AgentRuntime, MachineProfile};
use usb_control::{UsbPolicy, UsbPolicyResult};
use tauri::{Emitter, Manager, WindowEvent};
use tauri_plugin_store::Builder as StorePluginBuilder;
#[cfg(target_os = "windows")]
@ -100,6 +102,26 @@ fn run_rustdesk_ensure(
Err("Provisionamento automático do RustDesk está disponível apenas no Windows.".to_string())
}
#[tauri::command]
fn apply_usb_policy(policy: String) -> Result<UsbPolicyResult, String> {
let policy_enum = UsbPolicy::from_str(&policy)
.ok_or_else(|| format!("Politica USB invalida: {}. Use ALLOW, BLOCK_ALL ou READONLY.", policy))?;
usb_control::apply_usb_policy(policy_enum).map_err(|e| e.to_string())
}
#[tauri::command]
fn get_usb_policy() -> Result<String, String> {
usb_control::get_current_policy()
.map(|p| p.as_str().to_string())
.map_err(|e| e.to_string())
}
#[tauri::command]
fn refresh_usb_policy() -> Result<(), String> {
usb_control::refresh_group_policy().map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
@ -128,7 +150,10 @@ pub fn run() {
start_machine_agent,
stop_machine_agent,
open_devtools,
ensure_rustdesk_and_emit
ensure_rustdesk_and_emit,
apply_usb_policy,
get_usb_policy,
refresh_usb_policy
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View file

@ -0,0 +1,296 @@
//! USB Storage Control Module
//!
//! Este modulo implementa o controle de dispositivos de armazenamento USB no Windows.
//! Utiliza duas abordagens complementares:
//! 1. Removable Storage Access Policy (via registro do Windows)
//! 2. USBSTOR driver control (como fallback/reforco)
//!
//! IMPORTANTE: Requer privilegios de administrador para funcionar.
use serde::{Deserialize, Serialize};
use std::io;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum UsbPolicy {
Allow,
BlockAll,
Readonly,
}
impl UsbPolicy {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_uppercase().as_str() {
"ALLOW" => Some(Self::Allow),
"BLOCK_ALL" => Some(Self::BlockAll),
"READONLY" => Some(Self::Readonly),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Allow => "ALLOW",
Self::BlockAll => "BLOCK_ALL",
Self::Readonly => "READONLY",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UsbPolicyResult {
pub success: bool,
pub policy: String,
pub error: Option<String>,
pub applied_at: Option<i64>,
}
#[derive(Error, Debug)]
pub enum UsbControlError {
#[error("Politica USB invalida: {0}")]
InvalidPolicy(String),
#[error("Erro de registro do Windows: {0}")]
RegistryError(String),
#[error("Permissao negada - requer privilegios de administrador")]
PermissionDenied,
#[error("Sistema operacional nao suportado")]
UnsupportedOs,
#[error("Erro de I/O: {0}")]
Io(#[from] io::Error),
}
#[cfg(target_os = "windows")]
mod windows_impl {
use super::*;
use winreg::enums::*;
use winreg::RegKey;
// GUID para Removable Storage Devices (Disk)
const REMOVABLE_STORAGE_GUID: &str = "{53f56307-b6bf-11d0-94f2-00a0c91efb8b}";
// Chaves de registro
const REMOVABLE_STORAGE_PATH: &str =
r"Software\Policies\Microsoft\Windows\RemovableStorageDevices";
const USBSTOR_PATH: &str = r"SYSTEM\CurrentControlSet\Services\USBSTOR";
const STORAGE_POLICY_PATH: &str = r"SYSTEM\CurrentControlSet\Control\StorageDevicePolicies";
pub fn apply_usb_policy(policy: UsbPolicy) -> Result<UsbPolicyResult, UsbControlError> {
let now = chrono::Utc::now().timestamp_millis();
// 1. Aplicar Removable Storage Access Policy
apply_removable_storage_policy(policy)?;
// 2. Aplicar USBSTOR como reforco
apply_usbstor_policy(policy)?;
// 3. Aplicar WriteProtect se necessario
if policy == UsbPolicy::Readonly {
apply_write_protect(true)?;
} else {
apply_write_protect(false)?;
}
Ok(UsbPolicyResult {
success: true,
policy: policy.as_str().to_string(),
error: None,
applied_at: Some(now),
})
}
fn apply_removable_storage_policy(policy: UsbPolicy) -> Result<(), UsbControlError> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID);
match policy {
UsbPolicy::Allow => {
// Tenta remover as restricoes, se existirem
if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_ALL_ACCESS) {
let _ = key.delete_value("Deny_Read");
let _ = key.delete_value("Deny_Write");
let _ = key.delete_value("Deny_Execute");
}
// Tenta remover a chave inteira se estiver vazia
let _ = hklm.delete_subkey(&full_path);
}
UsbPolicy::BlockAll => {
let (key, _) = hklm
.create_subkey(&full_path)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
key.set_value("Deny_Read", &1u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
key.set_value("Deny_Write", &1u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
key.set_value("Deny_Execute", &1u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
}
UsbPolicy::Readonly => {
let (key, _) = hklm
.create_subkey(&full_path)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
// Permite leitura, bloqueia escrita
key.set_value("Deny_Read", &0u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
key.set_value("Deny_Write", &1u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
key.set_value("Deny_Execute", &0u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
}
}
Ok(())
}
fn apply_usbstor_policy(policy: UsbPolicy) -> Result<(), UsbControlError> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let key = hklm
.open_subkey_with_flags(USBSTOR_PATH, KEY_ALL_ACCESS)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
match policy {
UsbPolicy::Allow => {
// Start = 3 habilita o driver
key.set_value("Start", &3u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
}
UsbPolicy::BlockAll | UsbPolicy::Readonly => {
// Start = 4 desabilita o driver
// Nota: Para Readonly, mantemos o driver ativo mas com WriteProtect
// Porem, como fallback de seguranca, desabilitamos para BlockAll
if policy == UsbPolicy::BlockAll {
key.set_value("Start", &4u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
} else {
// Readonly mantem driver ativo
key.set_value("Start", &3u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
}
}
}
Ok(())
}
fn apply_write_protect(enable: bool) -> Result<(), UsbControlError> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
if enable {
let (key, _) = hklm
.create_subkey(STORAGE_POLICY_PATH)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
key.set_value("WriteProtect", &1u32)
.map_err(|e| UsbControlError::RegistryError(e.to_string()))?;
} else {
if let Ok(key) = hklm.open_subkey_with_flags(STORAGE_POLICY_PATH, KEY_ALL_ACCESS) {
let _ = key.set_value("WriteProtect", &0u32);
}
}
Ok(())
}
pub fn get_current_policy() -> Result<UsbPolicy, UsbControlError> {
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
// Verifica Removable Storage Policy primeiro
let full_path = format!(r"{}\{}", REMOVABLE_STORAGE_PATH, REMOVABLE_STORAGE_GUID);
if let Ok(key) = hklm.open_subkey_with_flags(&full_path, KEY_READ) {
let deny_read: u32 = key.get_value("Deny_Read").unwrap_or(0);
let deny_write: u32 = key.get_value("Deny_Write").unwrap_or(0);
if deny_read == 1 && deny_write == 1 {
return Ok(UsbPolicy::BlockAll);
}
if deny_read == 0 && deny_write == 1 {
return Ok(UsbPolicy::Readonly);
}
}
// Verifica USBSTOR como fallback
if let Ok(key) = hklm.open_subkey_with_flags(USBSTOR_PATH, KEY_READ) {
let start: u32 = key.get_value("Start").unwrap_or(3);
if start == 4 {
return Ok(UsbPolicy::BlockAll);
}
}
Ok(UsbPolicy::Allow)
}
pub fn refresh_group_policy() -> Result<(), UsbControlError> {
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
// Executa gpupdate para forcar atualizacao das politicas
let output = Command::new("gpupdate")
.args(["/target:computer", "/force"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| UsbControlError::Io(e))?;
if !output.status.success() {
// Nao e critico se falhar, apenas log
eprintln!(
"[usb_control] gpupdate retornou erro: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
}
#[cfg(not(target_os = "windows"))]
mod fallback_impl {
use super::*;
pub fn apply_usb_policy(_policy: UsbPolicy) -> Result<UsbPolicyResult, UsbControlError> {
Err(UsbControlError::UnsupportedOs)
}
pub fn get_current_policy() -> Result<UsbPolicy, UsbControlError> {
Err(UsbControlError::UnsupportedOs)
}
pub fn refresh_group_policy() -> Result<(), UsbControlError> {
Err(UsbControlError::UnsupportedOs)
}
}
#[cfg(target_os = "windows")]
pub use windows_impl::*;
#[cfg(not(target_os = "windows"))]
pub use fallback_impl::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_from_str() {
assert_eq!(UsbPolicy::from_str("ALLOW"), Some(UsbPolicy::Allow));
assert_eq!(UsbPolicy::from_str("BLOCK_ALL"), Some(UsbPolicy::BlockAll));
assert_eq!(UsbPolicy::from_str("READONLY"), Some(UsbPolicy::Readonly));
assert_eq!(UsbPolicy::from_str("allow"), Some(UsbPolicy::Allow));
assert_eq!(UsbPolicy::from_str("invalid"), None);
}
#[test]
fn test_policy_as_str() {
assert_eq!(UsbPolicy::Allow.as_str(), "ALLOW");
assert_eq!(UsbPolicy::BlockAll.as_str(), "BLOCK_ALL");
assert_eq!(UsbPolicy::Readonly.as_str(), "READONLY");
}
}