chore(hardening): tighten security and footprint defaults

This commit is contained in:
2026-04-24 19:03:57 +01:00
parent 55b34d8ffc
commit b333c6229e
36 changed files with 8702 additions and 254 deletions

View File

@@ -20,6 +20,7 @@ use kon_storage::{
};
use crate::commands::power::active_assertions_snapshot;
use crate::commands::security::ensure_main_window;
use crate::AppState;
const DEFAULT_RECENT_ERRORS: i64 = 50;
@@ -136,9 +137,11 @@ impl From<ErrorLogRow> for ErrorLogDto {
#[tauri::command]
pub async fn list_recent_errors_command(
window: tauri::WebviewWindow,
state: tauri::State<'_, AppState>,
limit: Option<i64>,
) -> Result<Vec<ErrorLogDto>, String> {
ensure_main_window(&window)?;
let n = limit.unwrap_or(DEFAULT_RECENT_ERRORS).clamp(1, 1000);
list_recent_errors(&state.db, n)
.await
@@ -157,7 +160,12 @@ pub struct CrashFile {
}
#[tauri::command]
pub async fn list_crash_files() -> Result<Vec<CrashFile>, String> {
pub async fn list_crash_files(window: tauri::WebviewWindow) -> Result<Vec<CrashFile>, String> {
ensure_main_window(&window)?;
list_crash_files_inner().await
}
async fn list_crash_files_inner() -> Result<Vec<CrashFile>, String> {
let dir = crashes_dir();
let entries = match fs::read_dir(&dir) {
Ok(e) => e,
@@ -215,12 +223,72 @@ impl Default for ReportOptions {
Self {
include_recent_errors: true,
include_crashes: true,
include_settings: true,
include_log_tail: true,
include_settings: false,
include_log_tail: false,
}
}
}
fn redact_home(input: &str) -> String {
match std::env::var("HOME") {
Ok(home) if !home.is_empty() => input.replace(&home, "~"),
_ => input.to_string(),
}
}
fn looks_sensitive_key(key: &str) -> bool {
let lower = key.to_ascii_lowercase();
lower.contains("path")
|| lower.contains("folder")
|| lower.contains("directory")
|| lower.contains("device")
|| lower.contains("microphone")
}
fn redact_json_value(value: &mut serde_json::Value, key_hint: Option<&str>) {
match value {
serde_json::Value::Object(map) => {
for (key, child) in map.iter_mut() {
redact_json_value(child, Some(key));
}
}
serde_json::Value::Array(items) => {
for item in items {
redact_json_value(item, key_hint);
}
}
serde_json::Value::String(text) => {
if key_hint.map(looks_sensitive_key).unwrap_or(false)
|| text.starts_with('/')
|| text.contains(":\\")
|| text.contains("/home/")
|| text.contains("/Users/")
{
*text = redact_home(text);
}
}
_ => {}
}
}
fn sanitise_preferences_json(raw: &str) -> String {
match serde_json::from_str::<serde_json::Value>(raw) {
Ok(mut value) => {
redact_json_value(&mut value, None);
serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string())
}
Err(_) => "_preferences could not be parsed as JSON_".to_string(),
}
}
fn redact_line(input: &str) -> String {
redact_home(input)
.chars()
.take(400)
.collect::<String>()
.replace('\n', " ")
}
/// Build a single human-readable diagnostic-report string. The user inspects
/// this in Settings → About before deciding whether to copy/save/email it.
///
@@ -228,8 +296,17 @@ impl Default for ReportOptions {
/// Discord thread, or a GitHub issue without further conversion.
#[tauri::command]
pub async fn generate_diagnostic_report(
window: tauri::WebviewWindow,
state: tauri::State<'_, AppState>,
options: Option<ReportOptions>,
) -> Result<String, String> {
ensure_main_window(&window)?;
generate_diagnostic_report_inner(&state, options).await
}
async fn generate_diagnostic_report_inner(
state: &tauri::State<'_, AppState>,
options: Option<ReportOptions>,
) -> Result<String, String> {
let opts = options.unwrap_or_default();
let mut out = String::new();
@@ -241,7 +318,10 @@ pub async fn generate_diagnostic_report(
std::env::consts::OS,
std::env::consts::ARCH
));
out.push_str(&format!("- App data dir: `{}`\n", app_data_dir().display()));
out.push_str(&format!(
"- App data dir: `{}`\n",
redact_home(&app_data_dir().display().to_string())
));
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
@@ -258,7 +338,7 @@ pub async fn generate_diagnostic_report(
match kon_storage::get_setting(&state.db, "kon_preferences").await {
Ok(Some(json)) => {
out.push_str("```json\n");
out.push_str(&json);
out.push_str(&sanitise_preferences_json(&json));
out.push_str("\n```\n\n");
}
Ok(None) => out.push_str("_(no preferences saved)_\n\n"),
@@ -278,11 +358,7 @@ pub async fn generate_diagnostic_report(
r.context,
r.error_code.as_deref().unwrap_or("?"),
if r.metadata.is_some() { " (+meta)" } else { "" },
r.message
.chars()
.take(400)
.collect::<String>()
.replace('\n', " "),
redact_line(&r.message),
));
}
out.push('\n');
@@ -307,7 +383,7 @@ pub async fn generate_diagnostic_report(
if opts.include_crashes {
out.push_str("## Crash dumps\n\n");
let crashes = list_crash_files().await.unwrap_or_default();
let crashes = list_crash_files_inner().await.unwrap_or_default();
if crashes.is_empty() {
out.push_str("_(no crash dumps on disk — nothing has crashed)_\n\n");
} else {
@@ -321,7 +397,7 @@ pub async fn generate_diagnostic_report(
out.push_str(&format!(
"_(plus {} older crash dumps in `{}`)_\n\n",
crashes.len() - 5,
crashes_dir().display()
redact_home(&crashes_dir().display().to_string())
));
}
}
@@ -336,7 +412,7 @@ pub async fn generate_diagnostic_report(
}
Ok(tail) => {
out.push_str("```\n");
out.push_str(&tail);
out.push_str(&redact_home(&tail));
out.push_str("\n```\n\n");
}
Err(_) => out.push_str("_(no log file found)_\n\n"),
@@ -435,14 +511,16 @@ pub fn get_os_info() -> OsInfo {
/// somewhere they can attach to an email.
#[tauri::command]
pub async fn save_diagnostic_report(
window: tauri::WebviewWindow,
app: tauri::AppHandle,
state: tauri::State<'_, AppState>,
options: Option<ReportOptions>,
) -> Result<String, String> {
ensure_main_window(&window)?;
let _ = app; // reserved for future dialog integration
let report = generate_diagnostic_report(state, options).await?;
let report = generate_diagnostic_report_inner(&state, options).await?;
let dir = app_data_dir().join("diagnostic-reports");
let dir = kon_storage::app_data_dir().join("diagnostic-reports");
fs::create_dir_all(&dir).map_err(|e| format!("create dir: {e}"))?;
let ts = SystemTime::now()