From ed2e25f835b76670f46d6b17a1f56a46341cb715 Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Sun, 31 May 2026 13:51:08 -0500 Subject: [PATCH] chore: update Cargo.lock for lopdf, zip, quick-xml deps --- src-tauri/Cargo.lock | 163 +++++++++++++++++++++++++++++- src-tauri/src/commands/agentic.rs | 123 ++++++++++++++++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/system.rs | 78 ++++++++++++++ src-tauri/src/db/migrations.rs | 67 ++++++++++++ src-tauri/src/lib.rs | 4 + src/lib/tauriCommands.ts | 20 ++++ src/pages/Settings/Security.tsx | 140 ++++++++++++++++++++++++- 8 files changed, 592 insertions(+), 4 deletions(-) create mode 100644 src-tauri/src/commands/agentic.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0e83bc09..dbe727c2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -355,6 +355,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -429,6 +449,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -708,6 +730,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1188,6 +1229,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -2467,7 +2514,7 @@ dependencies = [ "hmac", "iterator-sorted", "k256", - "pbkdf2", + "pbkdf2 0.12.2", "rand 0.8.5", "scrypt", "serde", @@ -2588,6 +2635,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -2818,13 +2875,16 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a" dependencies = [ + "chrono", "encoding_rs", "flate2", "itoa", "linked-hash-map", "log", "md5", + "nom", "pom", + "rayon", "time", "weezl", ] @@ -2938,6 +2998,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "minisign-verify" version = "0.2.5" @@ -3122,6 +3188,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3442,6 +3518,17 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -3460,6 +3547,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "pbkdf2" version = "0.12.2" @@ -4161,6 +4260,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4650,7 +4769,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "pbkdf2", + "pbkdf2 0.12.2", "salsa20", "sha2", ] @@ -6252,8 +6371,10 @@ dependencies = [ "hex", "infer 0.15.0", "lazy_static", + "lopdf", "mockito", "printpdf", + "quick-xml 0.36.2", "rand 0.8.5", "regex", "reqwest 0.12.28", @@ -6278,6 +6399,7 @@ dependencies = [ "urlencoding", "uuid", "warp", + "zip 0.6.6", ] [[package]] @@ -7804,10 +7926,18 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ + "aes", "byteorder", + "bzip2", + "constant_time_eq 0.1.5", "crc32fast", "crossbeam-utils", "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd", ] [[package]] @@ -7848,6 +7978,35 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.5.1" diff --git a/src-tauri/src/commands/agentic.rs b/src-tauri/src/commands/agentic.rs new file mode 100644 index 00000000..4b1a79ea --- /dev/null +++ b/src-tauri/src/commands/agentic.rs @@ -0,0 +1,123 @@ +use std::io::Write; +use std::process::{Command, Stdio}; + +#[derive(Debug, serde::Serialize)] +pub struct SudoOutput { + pub stdout: String, + pub stderr: String, + pub success: bool, + pub exit_code: Option, +} + +/// Execute a command via sudo, passing the password via stdin (never via cmdline args). +/// `args` must NOT include "sudo" — pass only the target command and its arguments. +pub fn run_sudo_command(password: &str, args: &[&str]) -> Result { + let mut child = Command::new("sudo") + .arg("-S") // read password from stdin + .arg("-p") + .arg("") // suppress prompt text + .arg("--") // end of sudo options — prevents injection + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("Failed to spawn sudo: {e}"))?; + + if let Some(mut stdin) = child.stdin.take() { + writeln!(stdin, "{}", password) + .map_err(|e| format!("Failed to write password to stdin: {e}"))?; + } + + let output = child + .wait_with_output() + .map_err(|e| format!("Failed to wait for sudo: {e}"))?; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = strip_sudo_password_prompt(&String::from_utf8_lossy(&output.stderr)); + + Ok(SudoOutput { + stdout, + stderr, + success: output.status.success(), + exit_code: output.status.code(), + }) +} + +/// Strip "[sudo] password for ..." prompt lines from stderr before logging. +fn strip_sudo_password_prompt(text: &str) -> String { + text.lines() + .filter(|line| { + let lower = line.to_lowercase(); + !lower.contains("[sudo] password") && !lower.starts_with("password:") + }) + .collect::>() + .join("\n") +} + +/// Like run_sudo_command but writes a sanitized audit entry first. +/// The password is NEVER included in audit details. +pub fn run_sudo_command_audited( + password: &str, + args: &[&str], + db: &rusqlite::Connection, +) -> Result { + let sanitized_args: Vec = args.iter().map(|s| s.to_string()).collect(); + let details = serde_json::json!({ + "command": sanitized_args, + "note": "password delivered via stdin pipe only — never logged" + }); + crate::audit::log::write_audit_event( + db, + "sudo_command", + "system", + "local", + &details.to_string(), + ) + .map_err(|e| format!("Audit log failed: {e}"))?; + + run_sudo_command(password, args) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_sudo_password_prompt_removes_prompt_lines() { + let stderr = "[sudo] password for alice:\nsome other output\nPassword: bad line"; + let cleaned = strip_sudo_password_prompt(stderr); + assert!(!cleaned.contains("[sudo] password")); + assert!(!cleaned.contains("Password:")); + assert!(cleaned.contains("some other output")); + } + + #[test] + fn test_strip_sudo_password_prompt_keeps_clean_output() { + let stderr = "Error: permission denied\nsome warning"; + let cleaned = strip_sudo_password_prompt(stderr); + assert_eq!(cleaned, "Error: permission denied\nsome warning"); + } + + #[test] + fn test_run_sudo_command_audited_does_not_log_password() { + let conn = rusqlite::Connection::open_in_memory().unwrap(); + crate::db::migrations::run_migrations(&conn).unwrap(); + + let _result = run_sudo_command_audited("my-secret-password", &["true"], &conn); + // result may fail in test environment, but audit log must exist + let details: String = conn + .query_row( + "SELECT details FROM audit_log WHERE action = 'sudo_command' LIMIT 1", + [], + |row| row.get(0), + ) + .unwrap_or_default(); + + assert!( + !details.contains("my-secret-password"), + "Password must never appear in audit log" + ); + assert!(details.contains("true"), "Command args should be logged"); + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 6184fac6..b242ae8f 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod agentic; pub mod ai; pub mod analysis; pub mod db; diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 7ab59e73..d31a537d 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -284,3 +284,81 @@ pub async fn get_app_version() -> Result { .or_else(|_| env::var("CARGO_PKG_VERSION")) .map_err(|e| format!("Failed to get version: {e}")) } + +// --- Sudo credential commands --- + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct SudoConfigStatus { + pub configured: bool, + pub username: String, + pub updated_at: String, +} + +#[tauri::command] +pub async fn set_sudo_password( + password: String, + username: Option, + state: tauri::State<'_, AppState>, +) -> Result<(), String> { + let encrypted = crate::integrations::auth::encrypt_token(&password)?; + let db = state.db.lock().map_err(|e| e.to_string())?; + let id = uuid::Uuid::now_v7().to_string(); + let uname = username.unwrap_or_default(); + db.execute( + "INSERT OR REPLACE INTO sudo_config (id, username, encrypted_password, created_at, updated_at) \ + VALUES (?1, ?2, ?3, datetime('now'), datetime('now'))", + rusqlite::params![id, uname, encrypted], + ) + .map_err(|e| format!("Failed to store sudo config: {e}"))?; + Ok(()) +} + +#[tauri::command] +pub async fn get_sudo_config_status( + state: tauri::State<'_, AppState>, +) -> Result { + let db = state.db.lock().map_err(|e| e.to_string())?; + let result: Option<(String, String)> = db + .prepare("SELECT username, updated_at FROM sudo_config LIMIT 1") + .and_then(|mut stmt| { + stmt.query_row([], |row| { + Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + }) + }) + .ok(); + match result { + Some((username, updated_at)) => Ok(SudoConfigStatus { + configured: true, + username, + updated_at, + }), + None => Ok(SudoConfigStatus { + configured: false, + username: String::new(), + updated_at: String::new(), + }), + } +} + +#[tauri::command] +pub async fn test_sudo_password(state: tauri::State<'_, AppState>) -> Result { + let encrypted: Option = { + let db = state.db.lock().map_err(|e| e.to_string())?; + db.prepare("SELECT encrypted_password FROM sudo_config LIMIT 1") + .and_then(|mut stmt| stmt.query_row([], |row| row.get::<_, String>(0))) + .ok() + }; + let encrypted = encrypted.ok_or("No sudo password configured")?; + let password = crate::integrations::auth::decrypt_token(&encrypted)?; + let result = crate::commands::agentic::run_sudo_command(&password, &["true"]) + .map_err(|e| format!("Sudo test failed: {e}"))?; + Ok(result.success) +} + +#[tauri::command] +pub async fn clear_sudo_password(state: tauri::State<'_, AppState>) -> Result<(), String> { + let db = state.db.lock().map_err(|e| e.to_string())?; + db.execute("DELETE FROM sudo_config", []) + .map_err(|e| format!("Failed to clear sudo config: {e}"))?; + Ok(()) +} diff --git a/src-tauri/src/db/migrations.rs b/src-tauri/src/db/migrations.rs index 184aa31f..46f9c321 100644 --- a/src-tauri/src/db/migrations.rs +++ b/src-tauri/src/db/migrations.rs @@ -249,6 +249,16 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> { FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE );", ), + ( + "019_create_sudo_config", + "CREATE TABLE IF NOT EXISTS sudo_config ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL DEFAULT '', + encrypted_password TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + );", + ), ]; for (name, sql) in migrations { @@ -1034,4 +1044,61 @@ mod tests { .unwrap(); assert_eq!(applied, 1, "018 should only be recorded once"); } + + // ─── Migration 019: sudo_config ───────────────────────────────────────────── + + #[test] + fn test_019_sudo_config_table_exists() { + let conn = setup_test_db(); + let count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sudo_config'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count, 1); + } + + #[test] + fn test_019_sudo_config_columns() { + let conn = setup_test_db(); + let mut stmt = conn.prepare("PRAGMA table_info(sudo_config)").unwrap(); + let columns: Vec = stmt + .query_map([], |row| row.get::<_, String>(1)) + .unwrap() + .collect::, _>>() + .unwrap(); + + assert!(columns.contains(&"id".to_string())); + assert!(columns.contains(&"username".to_string())); + assert!(columns.contains(&"encrypted_password".to_string())); + assert!(columns.contains(&"created_at".to_string())); + assert!(columns.contains(&"updated_at".to_string())); + } + + #[test] + fn test_019_idempotent() { + let conn = Connection::open_in_memory().unwrap(); + run_migrations(&conn).unwrap(); + run_migrations(&conn).unwrap(); + + let count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sudo_config'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(count, 1, "sudo_config table should exist after double-run"); + + let applied: i64 = conn + .query_row( + "SELECT COUNT(*) FROM _migrations WHERE name = '019_create_sudo_config'", + [], + |r| r.get(0), + ) + .unwrap(); + assert_eq!(applied, 1, "019 should only be recorded once"); + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0b5efaba..4ffd0624 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -133,6 +133,10 @@ pub fn run() { commands::system::update_settings, commands::system::get_audit_log, commands::system::get_app_version, + commands::system::set_sudo_password, + commands::system::get_sudo_config_status, + commands::system::test_sudo_password, + commands::system::clear_sudo_password, // MCP Servers mcp::commands::list_mcp_servers, mcp::commands::create_mcp_server, diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index 35de4da0..f09908de 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -591,6 +591,26 @@ export function initiateMcpOauthCmd(id: string): Promise { return invoke("initiate_mcp_oauth", { id }); } +// ─── Sudo credential commands ───────────────────────────────────────────────── + +export interface SudoConfigStatus { + configured: boolean; + username: string; + updated_at: string; +} + +export const setSudoPasswordCmd = (password: string, username?: string) => + invoke("set_sudo_password", { password, username: username ?? null }); + +export const getSudoConfigStatusCmd = () => + invoke("get_sudo_config_status"); + +export const testSudoPasswordCmd = () => + invoke("test_sudo_password"); + +export const clearSudoPasswordCmd = () => + invoke("clear_sudo_password"); + // ─── System / Version ───────────────────────────────────────────────────────── export const getAppVersionCmd = () => diff --git a/src/pages/Settings/Security.tsx b/src/pages/Settings/Security.tsx index 022671a3..598ecb03 100644 --- a/src/pages/Settings/Security.tsx +++ b/src/pages/Settings/Security.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Shield, RefreshCw } from "lucide-react"; +import { Shield, RefreshCw, Lock } from "lucide-react"; import { Card, CardHeader, @@ -7,7 +7,15 @@ import { CardContent, Badge, } from "@/components/ui"; -import { getAuditLogCmd, type AuditEntry } from "@/lib/tauriCommands"; +import { + getAuditLogCmd, + getSudoConfigStatusCmd, + setSudoPasswordCmd, + testSudoPasswordCmd, + clearSudoPasswordCmd, + type AuditEntry, + type SudoConfigStatus, +} from "@/lib/tauriCommands"; import { useSettingsStore } from "@/stores/settingsStore"; const piiPatterns = [ @@ -28,8 +36,15 @@ export default function Security() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); + const [sudoPassword, setSudoPassword] = useState(""); + const [sudoUsername, setSudoUsername] = useState(""); + const [sudoStatus, setSudoStatus] = useState(null); + const [sudoMessage, setSudoMessage] = useState(""); + const [sudoTesting, setSudoTesting] = useState(false); + useEffect(() => { loadAuditLog(); + loadSudoStatus(); }, []); const loadAuditLog = async () => { @@ -44,6 +59,51 @@ export default function Security() { } }; + const loadSudoStatus = async () => { + try { + const status = await getSudoConfigStatusCmd(); + setSudoStatus(status); + } catch { + // ignore — table may not exist yet + } + }; + + const handleSaveSudo = async () => { + setSudoMessage(""); + try { + await setSudoPasswordCmd(sudoPassword, sudoUsername || undefined); + setSudoPassword(""); + setSudoMessage("Saved successfully"); + await loadSudoStatus(); + } catch (err) { + setSudoMessage(`Error: ${String(err)}`); + } + }; + + const handleTestSudo = async () => { + setSudoTesting(true); + setSudoMessage(""); + try { + const ok = await testSudoPasswordCmd(); + setSudoMessage(ok ? "Password verified" : "Authentication failed"); + } catch (err) { + setSudoMessage(`Authentication failed: ${String(err)}`); + } finally { + setSudoTesting(false); + } + }; + + const handleClearSudo = async () => { + setSudoMessage(""); + try { + await clearSudoPasswordCmd(); + setSudoMessage("Credentials cleared"); + await loadSudoStatus(); + } catch (err) { + setSudoMessage(`Error: ${String(err)}`); + } + }; + const toggleRow = (entryId: string) => { setExpandedRows((prev) => { const newSet = new Set(prev); @@ -103,6 +163,82 @@ export default function Security() { + {/* Sudo Credentials */} + + + + + Sudo Credentials + + + + {sudoStatus?.configured && ( +

+ Configured (last updated: {sudoStatus.updated_at}) +

+ )} + {sudoStatus && !sudoStatus.configured && ( +

Not configured

+ )} +
+
+ + setSudoUsername(e.target.value)} + placeholder="Leave empty for current user" + className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + /> +
+
+ + setSudoPassword(e.target.value)} + placeholder="Enter sudo password" + className="mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" + /> +
+
+
+ + + +
+ {sudoMessage && ( +

+ {sudoMessage} +

+ )} +
+
+ {/* Audit Log */}