Add desktop tools, antivirus, and editor modules

- Add desktop tools module with drive cleaner, Windows optimizer, and
  Brave browser installer
- Add antivirus module with ClamAV integration and Windows Defender
  management
- Add tools.html template for settings page integration
- Add standalone editor.html for file editing
- Re-export new types from desktop and security
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-29 21:23:19 -03:00
parent a21292daa3
commit 46d6ff6268
6 changed files with 3412 additions and 0 deletions

View file

@ -1,6 +1,22 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
//! Desktop Module
//!
//! This module provides desktop-specific functionality including:
//! - Drive synchronization with cloud storage
//! - System tray management
//! - Local file operations
//! - Desktop tools (cleaner, optimizer, etc.)
pub mod drive;
pub mod sync;
pub mod tools;
pub mod tray;
// Re-exports
pub use drive::*;
pub use sync::*;
pub use tools::{
CleanupCategory, CleanupStats, DesktopToolsConfig, DesktopToolsManager, DiskInfo,
InstallationStatus, OptimizationStatus, OptimizationTask, TaskStatus,
};
pub use tray::{RunningMode, ServiceMonitor, TrayManager};

1002
src/desktop/tools.rs Normal file

File diff suppressed because it is too large Load diff

849
src/security/antivirus.rs Normal file
View file

@ -0,0 +1,849 @@
//! Antivirus Module
//!
//! This module provides antivirus and security scanning functionality including:
//! - Integration with ClamAV (open source antivirus)
//! - Windows Defender management (enable/disable)
//! - Threat detection and reporting
//! - Vulnerability scanning
//! - Real-time protection status
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, warn};
/// Threat severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThreatSeverity {
Low,
Medium,
High,
Critical,
}
/// Threat status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThreatStatus {
Detected,
Quarantined,
Removed,
Allowed,
Failed,
}
/// Detected threat information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Threat {
pub id: String,
pub name: String,
pub threat_type: String,
pub severity: ThreatSeverity,
pub status: ThreatStatus,
pub file_path: Option<String>,
pub detected_at: chrono::DateTime<chrono::Utc>,
pub description: Option<String>,
pub action_taken: Option<String>,
}
/// Vulnerability information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
pub id: String,
pub cve_id: Option<String>,
pub name: String,
pub severity: ThreatSeverity,
pub affected_component: String,
pub description: String,
pub remediation: Option<String>,
pub detected_at: chrono::DateTime<chrono::Utc>,
pub is_patched: bool,
}
/// Scan result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResult {
pub scan_id: String,
pub started_at: chrono::DateTime<chrono::Utc>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
pub status: ScanStatus,
pub files_scanned: u64,
pub threats_found: Vec<Threat>,
pub scan_type: ScanType,
pub target_path: Option<String>,
pub error_message: Option<String>,
}
/// Scan status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScanStatus {
Pending,
Running,
Completed,
Failed,
Cancelled,
}
/// Scan type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ScanType {
Quick,
Full,
Custom,
Memory,
Rootkit,
}
/// Protection status
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtectionStatus {
pub real_time_protection: bool,
pub windows_defender_enabled: bool,
pub general_bots_protection: bool,
pub last_scan: Option<chrono::DateTime<chrono::Utc>>,
pub last_definition_update: Option<chrono::DateTime<chrono::Utc>>,
pub threats_blocked_today: u32,
pub quarantined_items: u32,
}
/// Antivirus configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AntivirusConfig {
/// Path to ClamAV installation
pub clamav_path: Option<PathBuf>,
/// Enable real-time protection
pub real_time_protection: bool,
/// Quarantine directory
pub quarantine_dir: PathBuf,
/// Log directory
pub log_dir: PathBuf,
/// Auto-quarantine threats
pub auto_quarantine: bool,
/// Excluded paths from scanning
pub excluded_paths: Vec<PathBuf>,
/// Excluded file extensions
pub excluded_extensions: Vec<String>,
/// Maximum file size to scan (in MB)
pub max_file_size_mb: u64,
/// Scan archives
pub scan_archives: bool,
/// Definition update URL
pub definition_update_url: Option<String>,
}
impl Default for AntivirusConfig {
fn default() -> Self {
Self {
clamav_path: None,
real_time_protection: true,
quarantine_dir: PathBuf::from("./data/quarantine"),
log_dir: PathBuf::from("./logs/antivirus"),
auto_quarantine: true,
excluded_paths: vec![],
excluded_extensions: vec![],
max_file_size_mb: 100,
scan_archives: true,
definition_update_url: None,
}
}
}
/// Antivirus Manager
pub struct AntivirusManager {
config: AntivirusConfig,
threats: Arc<RwLock<Vec<Threat>>>,
vulnerabilities: Arc<RwLock<Vec<Vulnerability>>>,
active_scans: Arc<RwLock<HashMap<String, ScanResult>>>,
protection_status: Arc<RwLock<ProtectionStatus>>,
}
impl AntivirusManager {
/// Create a new antivirus manager
pub fn new(config: AntivirusConfig) -> Result<Self> {
// Ensure directories exist
std::fs::create_dir_all(&config.quarantine_dir)
.context("Failed to create quarantine directory")?;
std::fs::create_dir_all(&config.log_dir).context("Failed to create log directory")?;
let protection_status = ProtectionStatus {
real_time_protection: config.real_time_protection,
windows_defender_enabled: Self::check_windows_defender_status(),
general_bots_protection: true,
last_scan: None,
last_definition_update: None,
threats_blocked_today: 0,
quarantined_items: 0,
};
Ok(Self {
config,
threats: Arc::new(RwLock::new(Vec::new())),
vulnerabilities: Arc::new(RwLock::new(Vec::new())),
active_scans: Arc::new(RwLock::new(HashMap::new())),
protection_status: Arc::new(RwLock::new(protection_status)),
})
}
/// Check if Windows Defender is enabled
#[cfg(target_os = "windows")]
fn check_windows_defender_status() -> bool {
let output = Command::new("powershell")
.args([
"-Command",
"Get-MpPreference | Select-Object -ExpandProperty DisableRealtimeMonitoring",
])
.output();
match output {
Ok(output) => {
let result = String::from_utf8_lossy(&output.stdout);
!result.trim().eq_ignore_ascii_case("true")
}
Err(_) => false,
}
}
#[cfg(not(target_os = "windows"))]
fn check_windows_defender_status() -> bool {
false
}
/// Disable Windows Defender (requires admin privileges)
#[cfg(target_os = "windows")]
pub async fn disable_windows_defender(&self) -> Result<bool> {
info!("Attempting to disable Windows Defender...");
// This requires administrator privileges
let script = r#"
Set-MpPreference -DisableRealtimeMonitoring $true
Set-MpPreference -DisableBehaviorMonitoring $true
Set-MpPreference -DisableBlockAtFirstSeen $true
Set-MpPreference -DisableIOAVProtection $true
Set-MpPreference -DisablePrivacyMode $true
Set-MpPreference -SignatureDisableUpdateOnStartupWithoutEngine $true
Set-MpPreference -DisableArchiveScanning $true
Set-MpPreference -DisableIntrusionPreventionSystem $true
Set-MpPreference -DisableScriptScanning $true
"#;
let output = Command::new("powershell")
.args(["-Command", script])
.output()
.context("Failed to execute PowerShell command")?;
if output.status.success() {
let mut status = self.protection_status.write().await;
status.windows_defender_enabled = false;
info!("Windows Defender disabled successfully");
Ok(true)
} else {
let error = String::from_utf8_lossy(&output.stderr);
error!("Failed to disable Windows Defender: {}", error);
Err(anyhow::anyhow!(
"Failed to disable Windows Defender: {}",
error
))
}
}
#[cfg(not(target_os = "windows"))]
pub async fn disable_windows_defender(&self) -> Result<bool> {
warn!("Windows Defender management is only available on Windows");
Ok(false)
}
/// Enable Windows Defender (requires admin privileges)
#[cfg(target_os = "windows")]
pub async fn enable_windows_defender(&self) -> Result<bool> {
info!("Attempting to enable Windows Defender...");
let script = r#"
Set-MpPreference -DisableRealtimeMonitoring $false
Set-MpPreference -DisableBehaviorMonitoring $false
Set-MpPreference -DisableBlockAtFirstSeen $false
Set-MpPreference -DisableIOAVProtection $false
Set-MpPreference -DisableArchiveScanning $false
Set-MpPreference -DisableIntrusionPreventionSystem $false
Set-MpPreference -DisableScriptScanning $false
"#;
let output = Command::new("powershell")
.args(["-Command", script])
.output()
.context("Failed to execute PowerShell command")?;
if output.status.success() {
let mut status = self.protection_status.write().await;
status.windows_defender_enabled = true;
info!("Windows Defender enabled successfully");
Ok(true)
} else {
let error = String::from_utf8_lossy(&output.stderr);
error!("Failed to enable Windows Defender: {}", error);
Err(anyhow::anyhow!(
"Failed to enable Windows Defender: {}",
error
))
}
}
#[cfg(not(target_os = "windows"))]
pub async fn enable_windows_defender(&self) -> Result<bool> {
warn!("Windows Defender management is only available on Windows");
Ok(false)
}
/// Start a scan
pub async fn start_scan(
&self,
scan_type: ScanType,
target_path: Option<&str>,
) -> Result<String> {
let scan_id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now();
let scan_result = ScanResult {
scan_id: scan_id.clone(),
started_at: now,
completed_at: None,
status: ScanStatus::Pending,
files_scanned: 0,
threats_found: vec![],
scan_type,
target_path: target_path.map(|s| s.to_string()),
error_message: None,
};
{
let mut scans = self.active_scans.write().await;
scans.insert(scan_id.clone(), scan_result);
}
// Spawn scan task
let scan_id_clone = scan_id.clone();
let scans = self.active_scans.clone();
let threats = self.threats.clone();
let config = self.config.clone();
let target = target_path.map(|s| s.to_string());
tokio::spawn(async move {
Self::run_scan(scan_id_clone, scan_type, target, scans, threats, config).await;
});
info!("Started {:?} scan with ID: {}", scan_type, scan_id);
Ok(scan_id)
}
/// Run the actual scan
async fn run_scan(
scan_id: String,
scan_type: ScanType,
target_path: Option<String>,
scans: Arc<RwLock<HashMap<String, ScanResult>>>,
threats: Arc<RwLock<Vec<Threat>>>,
config: AntivirusConfig,
) {
// Update status to running
{
let mut scans_guard = scans.write().await;
if let Some(scan) = scans_guard.get_mut(&scan_id) {
scan.status = ScanStatus::Running;
}
}
// Determine scan target
let scan_path = match scan_type {
ScanType::Quick => target_path.unwrap_or_else(|| {
if cfg!(target_os = "windows") {
"C:\\Users".to_string()
} else {
"/home".to_string()
}
}),
ScanType::Full => target_path.unwrap_or_else(|| {
if cfg!(target_os = "windows") {
"C:\\".to_string()
} else {
"/".to_string()
}
}),
ScanType::Custom => target_path.unwrap_or_else(|| ".".to_string()),
ScanType::Memory => "memory".to_string(),
ScanType::Rootkit => "/".to_string(),
};
// Try ClamAV scan
let result = Self::run_clamav_scan(&scan_path, &config).await;
// Update scan results
let mut scans_guard = scans.write().await;
if let Some(scan) = scans_guard.get_mut(&scan_id) {
scan.completed_at = Some(chrono::Utc::now());
match result {
Ok((files_scanned, found_threats)) => {
scan.status = ScanStatus::Completed;
scan.files_scanned = files_scanned;
scan.threats_found = found_threats.clone();
// Add threats to global list
if !found_threats.is_empty() {
let mut threats_guard = threats.blocking_write();
threats_guard.extend(found_threats);
}
}
Err(e) => {
scan.status = ScanStatus::Failed;
scan.error_message = Some(e.to_string());
}
}
}
}
/// Run ClamAV scan
async fn run_clamav_scan(path: &str, config: &AntivirusConfig) -> Result<(u64, Vec<Threat>)> {
// Find clamscan executable
let clamscan = config
.clamav_path
.clone()
.map(|p| p.join("clamscan"))
.unwrap_or_else(|| {
if cfg!(target_os = "windows") {
PathBuf::from("C:\\Program Files\\ClamAV\\clamscan.exe")
} else {
PathBuf::from("/usr/bin/clamscan")
}
});
if !clamscan.exists() {
// Try system PATH
let output = Command::new("which")
.arg("clamscan")
.output()
.unwrap_or_else(|_| {
Command::new("where")
.arg("clamscan")
.output()
.unwrap_or_else(|_| std::process::Output {
status: std::process::ExitStatus::default(),
stdout: vec![],
stderr: vec![],
})
});
if output.stdout.is_empty() {
return Err(anyhow::anyhow!(
"ClamAV not found. Please install ClamAV first."
));
}
}
let mut cmd = Command::new(&clamscan);
cmd.arg("-r") // Recursive
.arg("--infected") // Only show infected files
.arg("--no-summary");
if config.scan_archives {
cmd.arg("--scan-archive=yes");
}
// Add excluded paths
for excluded in &config.excluded_paths {
cmd.arg(format!("--exclude-dir={}", excluded.display()));
}
cmd.arg(path);
let output = cmd.output().context("Failed to run ClamAV scan")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut threats = Vec::new();
let mut files_scanned: u64 = 0;
// Parse ClamAV output
for line in stdout.lines() {
if line.contains("FOUND") {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() >= 2 {
let file_path = parts[0].trim();
let threat_name = parts[1].trim().replace(" FOUND", "");
threats.push(Threat {
id: uuid::Uuid::new_v4().to_string(),
name: threat_name.clone(),
threat_type: Self::classify_threat(&threat_name),
severity: Self::assess_severity(&threat_name),
status: ThreatStatus::Detected,
file_path: Some(file_path.to_string()),
detected_at: chrono::Utc::now(),
description: Some(format!("Detected by ClamAV: {}", threat_name)),
action_taken: None,
});
}
}
files_scanned += 1;
}
Ok((files_scanned, threats))
}
/// Classify threat type
fn classify_threat(name: &str) -> String {
let name_lower = name.to_lowercase();
if name_lower.contains("trojan") {
"Trojan".to_string()
} else if name_lower.contains("virus") {
"Virus".to_string()
} else if name_lower.contains("worm") {
"Worm".to_string()
} else if name_lower.contains("ransomware") {
"Ransomware".to_string()
} else if name_lower.contains("spyware") {
"Spyware".to_string()
} else if name_lower.contains("adware") {
"Adware".to_string()
} else if name_lower.contains("rootkit") {
"Rootkit".to_string()
} else if name_lower.contains("pup") || name_lower.contains("pua") {
"PUP".to_string()
} else {
"Malware".to_string()
}
}
/// Assess threat severity
fn assess_severity(name: &str) -> ThreatSeverity {
let name_lower = name.to_lowercase();
if name_lower.contains("ransomware") || name_lower.contains("rootkit") {
ThreatSeverity::Critical
} else if name_lower.contains("trojan") || name_lower.contains("backdoor") {
ThreatSeverity::High
} else if name_lower.contains("virus") || name_lower.contains("worm") {
ThreatSeverity::Medium
} else {
ThreatSeverity::Low
}
}
/// Quarantine a file
pub async fn quarantine_file(&self, file_path: &Path) -> Result<()> {
if !file_path.exists() {
return Err(anyhow::anyhow!("File not found: {:?}", file_path));
}
let file_name = file_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let quarantine_path = self.config.quarantine_dir.join(format!(
"{}_{}",
chrono::Utc::now().timestamp(),
file_name
));
std::fs::rename(file_path, &quarantine_path)
.context("Failed to move file to quarantine")?;
info!("File quarantined: {:?} -> {:?}", file_path, quarantine_path);
// Update protection status
let mut status = self.protection_status.write().await;
status.quarantined_items += 1;
Ok(())
}
/// Remove a threat
pub async fn remove_threat(&self, threat_id: &str) -> Result<()> {
let mut threats = self.threats.write().await;
if let Some(pos) = threats.iter().position(|t| t.id == threat_id) {
let threat = &threats[pos];
if let Some(ref file_path) = threat.file_path {
let path = Path::new(file_path);
if path.exists() {
std::fs::remove_file(path).context("Failed to remove infected file")?;
info!("Removed infected file: {}", file_path);
}
}
threats[pos].status = ThreatStatus::Removed;
threats[pos].action_taken = Some("File removed".to_string());
}
Ok(())
}
/// Get all detected threats
pub async fn get_threats(&self) -> Vec<Threat> {
self.threats.read().await.clone()
}
/// Get threats by status
pub async fn get_threats_by_status(&self, status: ThreatStatus) -> Vec<Threat> {
self.threats
.read()
.await
.iter()
.filter(|t| t.status == status)
.cloned()
.collect()
}
/// Get all vulnerabilities
pub async fn get_vulnerabilities(&self) -> Vec<Vulnerability> {
self.vulnerabilities.read().await.clone()
}
/// Scan for vulnerabilities
pub async fn scan_vulnerabilities(&self) -> Result<Vec<Vulnerability>> {
let mut vulnerabilities = Vec::new();
// Check for common vulnerabilities
// 1. Check for outdated software (Windows)
#[cfg(target_os = "windows")]
{
// Check Windows Update status
let output = Command::new("powershell")
.args([
"-Command",
"Get-HotFix | Sort-Object -Property InstalledOn -Descending | Select-Object -First 1",
])
.output();
if let Ok(output) = output {
let result = String::from_utf8_lossy(&output.stdout);
// Check if updates are old
if result.is_empty() {
vulnerabilities.push(Vulnerability {
id: uuid::Uuid::new_v4().to_string(),
cve_id: None,
name: "Missing Windows Updates".to_string(),
severity: ThreatSeverity::High,
affected_component: "Windows Update".to_string(),
description: "System may be missing critical security updates".to_string(),
remediation: Some(
"Run Windows Update to install latest patches".to_string(),
),
detected_at: chrono::Utc::now(),
is_patched: false,
});
}
}
}
// 2. Check for weak file permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let sensitive_paths = vec!["/etc/passwd", "/etc/shadow", "/etc/ssh/sshd_config"];
for path_str in sensitive_paths {
let path = Path::new(path_str);
if path.exists() {
if let Ok(metadata) = std::fs::metadata(path) {
let mode = metadata.permissions().mode();
if mode & 0o002 != 0 {
// World writable
vulnerabilities.push(Vulnerability {
id: uuid::Uuid::new_v4().to_string(),
cve_id: None,
name: format!("Weak permissions on {}", path_str),
severity: ThreatSeverity::High,
affected_component: path_str.to_string(),
description: "Sensitive file has world-writable permissions"
.to_string(),
remediation: Some(format!("chmod o-w {}", path_str)),
detected_at: chrono::Utc::now(),
is_patched: false,
});
}
}
}
}
}
// Store vulnerabilities
{
let mut vulns = self.vulnerabilities.write().await;
vulns.extend(vulnerabilities.clone());
}
Ok(vulnerabilities)
}
/// Get protection status
pub async fn get_protection_status(&self) -> ProtectionStatus {
self.protection_status.read().await.clone()
}
/// Get scan result by ID
pub async fn get_scan_result(&self, scan_id: &str) -> Option<ScanResult> {
self.active_scans.read().await.get(scan_id).cloned()
}
/// Get all scans
pub async fn get_all_scans(&self) -> Vec<ScanResult> {
self.active_scans.read().await.values().cloned().collect()
}
/// Cancel a scan
pub async fn cancel_scan(&self, scan_id: &str) -> Result<()> {
let mut scans = self.active_scans.write().await;
if let Some(scan) = scans.get_mut(scan_id) {
if scan.status == ScanStatus::Running || scan.status == ScanStatus::Pending {
scan.status = ScanStatus::Cancelled;
scan.completed_at = Some(chrono::Utc::now());
info!("Scan {} cancelled", scan_id);
Ok(())
} else {
Err(anyhow::anyhow!("Scan is not running"))
}
} else {
Err(anyhow::anyhow!("Scan not found"))
}
}
/// Update virus definitions
pub async fn update_definitions(&self) -> Result<()> {
info!("Updating virus definitions...");
// Try freshclam (ClamAV updater)
let freshclam = if cfg!(target_os = "windows") {
"freshclam.exe"
} else {
"freshclam"
};
let output = Command::new(freshclam)
.output()
.context("Failed to run freshclam")?;
if output.status.success() {
let mut status = self.protection_status.write().await;
status.last_definition_update = Some(chrono::Utc::now());
info!("Virus definitions updated successfully");
Ok(())
} else {
let error = String::from_utf8_lossy(&output.stderr);
Err(anyhow::anyhow!("Failed to update definitions: {}", error))
}
}
/// Set real-time protection
pub async fn set_realtime_protection(&self, enabled: bool) -> Result<()> {
let mut status = self.protection_status.write().await;
status.real_time_protection = enabled;
info!("Real-time protection set to: {}", enabled);
Ok(())
}
}
/// API response types for the security endpoints
pub mod api {
use super::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct ThreatsResponse {
pub threats: Vec<Threat>,
pub total: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VulnerabilitiesResponse {
pub vulnerabilities: Vec<Vulnerability>,
pub total: usize,
pub critical: usize,
pub high: usize,
pub medium: usize,
pub low: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScanRequest {
pub scan_type: ScanType,
pub target_path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScanResponse {
pub scan_id: String,
pub status: ScanStatus,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ActionResponse {
pub success: bool,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DefenderStatusRequest {
pub enabled: bool,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_classify_threat() {
assert_eq!(
AntivirusManager::classify_threat("Win.Trojan.Generic"),
"Trojan"
);
assert_eq!(
AntivirusManager::classify_threat("Ransomware.WannaCry"),
"Ransomware"
);
assert_eq!(
AntivirusManager::classify_threat("PUP.Optional.Adware"),
"PUP"
);
assert_eq!(
AntivirusManager::classify_threat("Unknown.Malware"),
"Malware"
);
}
#[test]
fn test_assess_severity() {
assert_eq!(
AntivirusManager::assess_severity("Ransomware.Test"),
ThreatSeverity::Critical
);
assert_eq!(
AntivirusManager::assess_severity("Trojan.Generic"),
ThreatSeverity::High
);
assert_eq!(
AntivirusManager::assess_severity("Virus.Test"),
ThreatSeverity::Medium
);
assert_eq!(
AntivirusManager::assess_severity("PUP.Adware"),
ThreatSeverity::Low
);
}
#[tokio::test]
async fn test_antivirus_manager_creation() {
let config = AntivirusConfig::default();
let manager = AntivirusManager::new(config);
assert!(manager.is_ok());
}
}

View file

@ -6,12 +6,19 @@
//! - Internal Certificate Authority (CA) management
//! - Certificate lifecycle management
//! - Security utilities and helpers
//! - Antivirus and threat detection (ClamAV integration)
//! - Windows Defender management
pub mod antivirus;
pub mod ca;
pub mod integration;
pub mod mutual_tls;
pub mod tls;
pub use antivirus::{
AntivirusConfig, AntivirusManager, ProtectionStatus, ScanResult, ScanStatus, ScanType, Threat,
ThreatSeverity, ThreatStatus, Vulnerability,
};
pub use ca::{CaConfig, CaManager, CertificateRequest, CertificateResponse};
pub use integration::{
create_https_client, get_tls_integration, init_tls_integration, to_secure_url, TlsIntegration,

1019
templates/tools.html Normal file

File diff suppressed because it is too large Load diff

519
ui/suite/editor.html Normal file
View file

@ -0,0 +1,519 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Editor - General Bots</title>
<link rel="stylesheet" href="css/app.css" />
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--accent-color: #3b82f6;
--accent-hover: #2563eb;
--border-color: #475569;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
}
.editor-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.editor-title {
display: flex;
align-items: center;
gap: 12px;
}
.editor-title-icon {
font-size: 24px;
}
.editor-title-text {
font-size: 16px;
font-weight: 600;
}
.editor-path {
font-size: 12px;
color: var(--text-secondary);
}
.editor-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.toolbar-group {
display: flex;
align-items: center;
gap: 4px;
padding-right: 12px;
border-right: 1px solid var(--border-color);
}
.toolbar-group:last-child {
border-right: none;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn:hover {
background: var(--border-color);
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-small {
padding: 6px 10px;
font-size: 12px;
}
.editor-content {
flex: 1;
display: flex;
overflow: hidden;
}
.editor-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
.line-numbers {
width: 50px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
padding: 16px 8px;
overflow: hidden;
text-align: right;
user-select: none;
font-family: "Consolas", monospace;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
}
.text-editor {
flex: 1;
background: var(--bg-primary);
color: var(--text-primary);
border: none;
padding: 16px;
font-family: "Consolas", monospace;
font-size: 13px;
line-height: 1.6;
resize: none;
outline: none;
white-space: pre;
overflow: auto;
tab-size: 4;
}
.csv-editor {
flex: 1;
overflow: auto;
padding: 16px;
}
.csv-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.csv-table th,
.csv-table td {
border: 1px solid var(--border-color);
padding: 0;
min-width: 120px;
}
.csv-table th {
background: var(--bg-tertiary);
font-weight: 600;
}
.csv-table .row-num {
width: 40px;
min-width: 40px;
background: var(--bg-secondary);
color: var(--text-secondary);
text-align: center;
padding: 8px 4px;
font-size: 12px;
}
.csv-input {
width: 100%;
background: transparent;
border: none;
color: var(--text-primary);
padding: 8px 12px;
font-size: 13px;
outline: none;
}
.csv-input:focus {
background: var(--bg-secondary);
}
.status-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 16px;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
font-size: 12px;
color: var(--text-secondary);
}
.status-left,
.status-right {
display: flex;
align-items: center;
gap: 16px;
}
.dirty-indicator {
width: 8px;
height: 8px;
background: var(--warning);
border-radius: 50%;
margin-left: 8px;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-block;
}
.spinner {
width: 14px;
height: 14px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.notification {
position: fixed;
bottom: 60px;
right: 20px;
padding: 12px 20px;
background: var(--bg-tertiary);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border-left: 4px solid var(--accent-color);
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification.success {
border-left-color: var(--success);
}
.notification.error {
border-left-color: var(--error);
}
</style>
</head>
<body>
<div class="editor-container">
<!-- Header -->
<div class="editor-header">
<div class="editor-title">
<span class="editor-title-icon">📝</span>
<div>
<span
class="editor-title-text"
id="editor-filename"
hx-get="/api/v1/editor/filename"
hx-trigger="load"
hx-swap="innerHTML"></div>
Untitled
</span>
<div
class="editor-path"
id="editor-filepath"
hx-get="/api/v1/editor/filepath"
hx-trigger="load"
hx-swap="innerHTML">
</div>
</div>
<span
class="dirty-indicator"
id="dirty-indicator"
style="display: none;"
title="Unsaved changes">
</span>
</div>
<div>
<a href="#drive"
class="btn btn-small"
hx-get="/api/drive/list"
hx-target="#main-content"
hx-push-url="true">
✕ Close
</a>
</div>
</div>
<!-- Toolbar -->
<div class="editor-toolbar">
<div class="toolbar-group">
<button
class="btn btn-primary btn-small"
hx-post="/api/v1/editor/save"
hx-include="#text-editor"
hx-indicator="#save-spinner"
hx-swap="none"
hx-on::after-request="showSaveNotification(event)">
<span class="htmx-indicator spinner" id="save-spinner"></span>
💾 Save
</button>
<button
class="btn btn-small"
hx-get="/api/v1/editor/save-as"
hx-target="#save-dialog"
hx-swap="innerHTML">
Save As
</button>
</div>
<div class="toolbar-group">
<button
class="btn btn-small"
hx-post="/api/v1/editor/undo"
hx-target="#editor-content"
hx-swap="innerHTML">
↩️ Undo
</button>
<button
class="btn btn-small"
hx-post="/api/v1/editor/redo"
hx-target="#editor-content"
hx-swap="innerHTML">
↪️ Redo
</button>
</div>
<div class="toolbar-group" id="text-tools">
<button
class="btn btn-small"
hx-post="/api/v1/editor/format"
hx-include="#text-editor"
hx-target="#text-editor"
hx-swap="innerHTML">
{ } Format
</button>
</div>
<div class="toolbar-group" id="csv-tools" style="display: none;">
<button
class="btn btn-small"
hx-post="/api/v1/editor/csv/add-row"
hx-target="#csv-table-body"
hx-swap="beforeend">
Row
</button>
<button
class="btn btn-small"
hx-post="/api/v1/editor/csv/add-column"
hx-target="#csv-editor"
hx-swap="innerHTML">
Column
</button>
</div>
</div>
<!-- Editor Content - loaded via HTMX based on file type -->
<div class="editor-content" id="editor-content">
<!-- Text Editor (default) -->
<div class="editor-wrapper" id="text-editor-wrapper">
<div
class="line-numbers"
id="line-numbers"
hx-get="/api/v1/editor/line-numbers"
hx-trigger="keyup from:#text-editor delay:100ms"
hx-swap="innerHTML">
1
</div>
<textarea
class="text-editor"
id="text-editor"
name="content"
spellcheck="false"
hx-post="/api/v1/editor/autosave"
hx-trigger="keyup changed delay:5s"
hx-swap="none"
hx-indicator="#autosave-indicator"
placeholder="Start typing or open a file..."></textarea>
</div>
<!-- CSV Editor (shown for .csv files) -->
<div class="csv-editor" id="csv-editor" style="display: none;">
<table class="csv-table">
<thead id="csv-table-head">
<tr>
<th class="row-num">#</th>
<th>
<input
type="text"
class="csv-input"
name="header_0"
value="Column 1"
hx-post="/api/v1/editor/csv/update-header"
hx-trigger="change"
hx-swap="none">
</th>
</tr>
</thead>
<tbody
id="csv-table-body"
hx-get="/api/v1/editor/csv/rows"
hx-trigger="load"
hx-swap="innerHTML">
</tbody>
</table>
</div>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-left">
<span
id="file-type"
hx-get="/api/v1/editor/filetype"
hx-trigger="load"
hx-swap="innerHTML">
📄 Plain Text
</span>
<span>UTF-8</span>
<span
id="autosave-indicator"
class="htmx-indicator"
style="font-size: 11px;">
Saving...
</span>
</div>
<div class="status-right">
<span
id="cursor-position"
hx-get="/api/v1/editor/position"
hx-trigger="click from:#text-editor, keyup from:#text-editor"
hx-swap="innerHTML">
Ln 1, Col 1
</span>
</div>
</div>
</div>
<!-- Save Dialog (loaded via HTMX) -->
<div id="save-dialog"></div>
<!-- Notification -->
<div class="notification" id="notification"></div>
<script>
// Minimal JS for notification display (could be replaced with htmx extension)
function showSaveNotification(event) {
const notification = document.getElementById('notification');
if (event.detail.successful) {
notification.textContent = '✓ File saved';
notification.className = 'notification success show';
document.getElementById('dirty-indicator').style.display = 'none';
} else {
notification.textContent = '✗ Save failed';
notification.className = 'notification error show';
}
setTimeout(() => notification.classList.remove('show'), 3000);
}
// Mark as dirty on edit
document.getElementById('text-editor')?.addEventListener('input', function() {
document.getElementById('dirty-indicator').style.display = 'inline-block';
});
// Keyboard shortcuts using htmx triggers
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
htmx.trigger(document.querySelector('[hx-post="/api/v1/editor/save"]'), 'click');
}
});
</script>
</body>
</html>