chore(hardening): tighten security and footprint defaults
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user