diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index a94135c6..2843bfeb 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -13,22 +13,27 @@ pub async fn analyze_logs( provider_config: ProviderConfig, state: State<'_, AppState>, ) -> Result { - // Load log file contents + // Load log file contents — only redacted files may be sent to an AI provider let mut log_contents = String::new(); { let db = state.db.lock().map_err(|e| e.to_string())?; for file_id in &log_file_ids { let mut stmt = db - .prepare("SELECT file_name, file_path FROM log_files WHERE id = ?1") + .prepare("SELECT file_name, file_path, redacted FROM log_files WHERE id = ?1") .map_err(|e| e.to_string())?; - if let Ok((name, path)) = stmt.query_row([file_id], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + if let Ok((name, path, redacted)) = stmt.query_row([file_id], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, i32>(2)? != 0, + )) }) { + let redacted_path = redacted_path_for(&name, &path, redacted)?; log_contents.push_str(&format!("--- {name} ---\n")); - if let Ok(content) = std::fs::read_to_string(&path) { + if let Ok(content) = std::fs::read_to_string(&redacted_path) { log_contents.push_str(&content); } else { - log_contents.push_str("[Could not read file]\n"); + log_contents.push_str("[Could not read redacted file]\n"); } log_contents.push('\n'); } @@ -103,6 +108,17 @@ pub async fn analyze_logs( }) } +/// Returns the path to the `.redacted` file, or an error if the file has not been redacted. +fn redacted_path_for(name: &str, path: &str, redacted: bool) -> Result { + if !redacted { + return Err(format!( + "Log file '{name}' has not been scanned and redacted. \ + Run PII detection and apply redactions before sending to AI." + )); + } + Ok(format!("{path}.redacted")) +} + fn extract_section(text: &str, header: &str) -> Option { let start = text.find(header)?; let after = &text[start + header.len()..]; @@ -384,6 +400,19 @@ mod tests { assert_eq!(list, vec!["Item one", "Item two"]); } + #[test] + fn test_redacted_path_rejects_unredacted_file() { + let err = redacted_path_for("app.log", "/data/app.log", false).unwrap_err(); + assert!(err.contains("app.log")); + assert!(err.contains("redacted")); + } + + #[test] + fn test_redacted_path_returns_dotredacted_suffix() { + let path = redacted_path_for("app.log", "/data/app.log", true).unwrap(); + assert_eq!(path, "/data/app.log.redacted"); + } + #[test] fn test_extract_list_missing_header() { let text = "No findings here";