// This file is part of Deja-Vu. // // Deja-Vu is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Deja-Vu is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Deja-Vu. If not, see . use core::{action::DiagResult, line::DVLine, tasks::TaskResult}; use std::{fmt, sync::OnceLock, thread::sleep, time::Duration}; use ratatui::style::Color; use regex::Regex; use tracing::{info, warn}; pub struct RegexList { bitlocker_locked: OnceLock, bitlocker_off: OnceLock, carriage_returns: OnceLock, chkdsk_splits: OnceLock, } impl RegexList { fn bitlocker_locked(&self) -> &Regex { self.bitlocker_locked .get_or_init(|| Regex::new(r"Lock Status:\s+Locked").unwrap()) } fn bitlocker_off(&self) -> &Regex { self.bitlocker_off .get_or_init(|| Regex::new(r"Protection Status:\s+Protection Off").unwrap()) } fn carriage_returns(&self) -> &Regex { self.carriage_returns .get_or_init(|| Regex::new(r"^.*\r").unwrap()) } fn chkdsk_splits(&self) -> &Regex { self.chkdsk_splits.get_or_init(|| { Regex::new( r"(?m)^(WARNING|Stage |Windows |\d+.* total disk space|.*each allocation unit|Total duration)", ) .unwrap() }) } } static REGEXES: RegexList = RegexList { bitlocker_locked: OnceLock::new(), bitlocker_off: OnceLock::new(), carriage_returns: OnceLock::new(), chkdsk_splits: OnceLock::new(), }; #[derive(Clone, Copy, Debug)] pub enum Type { Bitlocker, BootConfigData, CheckDisk, ComponentStore, Registry, SystemFiles, Unknown, } impl fmt::Display for Type { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Type::Bitlocker => write!(f, "Bitlocker"), Type::BootConfigData => write!(f, "Boot Files"), Type::CheckDisk => write!(f, "CHKDSK"), Type::ComponentStore => write!(f, "DISM ScanHealth"), Type::Registry => write!(f, "Registry"), Type::SystemFiles => write!(f, "System Files"), Type::Unknown => write!(f, "Unknown Type"), } } } #[derive(Clone, Debug)] pub struct Log { pub label: String, pub summary: Vec, pub raw: String, } #[derive(Clone, Debug)] pub struct DiagGroup { pub diag_type: Type, pub passed: Vec, pub logs: Vec, pub result: String, } impl DiagGroup { pub fn new(diag_type: Type) -> Self { DiagGroup { diag_type, passed: Vec::new(), logs: Vec::new(), result: String::new(), } } pub fn get_logs_raw(&self) -> String { let raw_logs: Vec = self .logs .iter() .map(|log| format!("-- {} --\n{}", &log.label, &log.raw)) .collect(); raw_logs.join("\n\n\n") } pub fn get_logs_summary(&self) -> Vec { let mut summaries: Vec = Vec::new(); self.logs .iter() .for_each(|log| summaries.extend(log.summary.clone().into_iter())); summaries } pub fn get_pass_fail_warn(&self) -> DiagResult { let all_passed = self.passed.iter().fold(true, |acc, result| acc && *result); let all_failed = self.passed.iter().fold(true, |acc, result| acc && !*result); if all_passed { DiagResult::Pass } else if all_failed { DiagResult::Fail } else { DiagResult::Warn } } } pub fn get_diag_type(label: &str) -> Type { info!("Getting Diag type for {label}"); match label { "Bitlocker" => Type::Bitlocker, "Boot Files" => Type::BootConfigData, "DISM ScanHealth" => Type::ComponentStore, "CHKDSK" => Type::CheckDisk, "Registry" => Type::Registry, "System Files" => Type::SystemFiles, _ => { warn!("Failed to determine type"); Type::Unknown } } } pub fn parse_bcd(diag_group: &mut DiagGroup, task_result: TaskResult) { if !cfg!(windows) { sleep(Duration::from_millis(500)); return (); } match task_result { TaskResult::Error(err) => { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("BCD"), summary: vec![DVLine { line_parts: vec![String::from("BCD: "), String::from("Error")], line_colors: vec![Color::Reset, Color::Red], }], raw: err, }); } TaskResult::Output(stdout, stderr, passed) => { let output_text = if stderr.is_empty() { stdout.clone() } else { format!("{stdout}\n\n-------\n\n{stderr}") }; if passed { diag_group.passed.push(true); diag_group.result = String::from("OK"); diag_group.logs.push(Log { label: String::from("BCD"), summary: vec![DVLine { line_parts: vec![String::from("BCD: "), String::from("OK")], line_colors: vec![Color::Reset, Color::Green], }], raw: output_text, }); } else { diag_group.passed.push(false); diag_group.result = String::from("Unknown"); diag_group.logs.push(Log { label: String::from("BCD"), summary: vec![DVLine { line_parts: vec![String::from("BCD: "), String::from("Unknown")], line_colors: vec![Color::Reset, Color::Yellow], }], raw: output_text, }); } } } } pub fn parse_bitlocker(diag_group: &mut DiagGroup, task_result: TaskResult) { if !cfg!(windows) { sleep(Duration::from_millis(500)); return (); } let re_bitlocker_locked = REGEXES.bitlocker_locked(); let re_bitlocker_off = REGEXES.bitlocker_off(); match task_result { TaskResult::Error(err) => { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("Bitlocker"), summary: vec![DVLine { line_parts: vec![String::from("Bitlocker: "), String::from("Error")], line_colors: vec![Color::Reset, Color::Red], }], raw: err, }); } TaskResult::Output(stdout, stderr, _) => { let output_text = if stderr.is_empty() { stdout.clone() } else { format!("{stdout}\n\n-------\n\n{stderr}") }; let result: String; let set_result_text = !output_text.contains("All Key Protectors"); if re_bitlocker_off.is_match(&output_text) { diag_group.passed.push(true); result = String::from("OK"); diag_group.logs.push(Log { label: String::from("Bitlocker"), summary: vec![DVLine { line_parts: vec![String::from("Bitlocker: "), String::from("OFF")], line_colors: vec![Color::Reset, Color::Green], }], raw: output_text, }); } else if re_bitlocker_locked.is_match(&output_text) { diag_group.passed.push(false); result = String::from("Locked"); diag_group.logs.push(Log { label: String::from("Bitlocker"), summary: vec![DVLine { line_parts: vec![String::from("Bitlocker: "), String::from("Locked")], line_colors: vec![Color::Reset, Color::Red], }], raw: output_text, }); } else { diag_group.passed.push(true); result = String::from("Unknown"); diag_group.logs.push(Log { label: String::from("Bitlocker"), summary: Vec::new(), raw: output_text, }); } if set_result_text { diag_group.result = result; } } } } pub fn parse_chkdsk(diag_group: &mut DiagGroup, task_result: TaskResult) { if !cfg!(windows) { sleep(Duration::from_millis(500)); return (); } match task_result { TaskResult::Error(err) => { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("CHKDSK"), summary: vec![DVLine { line_parts: vec![String::from("CHKDSK: "), String::from("Error")], line_colors: vec![Color::Reset, Color::Red], }], raw: err, }); } TaskResult::Output(stdout, stderr, _) => { let re_chkdsk_splits = REGEXES.chkdsk_splits(); let output_text = if stderr.is_empty() { stdout.clone() } else { format!("{stdout}\n\n-------\n\n{stderr}") }; let output_text = remove_carriage_returns(&output_text); let parsed_output = re_chkdsk_splits .replace_all(output_text.as_str(), "\n $1") .to_string(); if parsed_output.contains("Windows has scanned the file system and found no problems.") { diag_group.passed.push(true); diag_group.result = String::from("OK"); diag_group.logs.push(Log { label: String::from("CHKDSK"), summary: vec![DVLine { line_parts: vec![String::from("CHKDSK: "), String::from("OK")], line_colors: vec![Color::Reset, Color::Green], }], raw: parsed_output, }); } else { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("CHKDSK"), summary: vec![DVLine { line_parts: vec![String::from("CHKDSK: "), String::from("Error")], line_colors: vec![Color::Reset, Color::Red], }], raw: parsed_output, }); } } } } pub fn parse_dism(diag_group: &mut DiagGroup, task_result: TaskResult) { if !cfg!(windows) { sleep(Duration::from_millis(500)); return (); } match task_result { TaskResult::Error(err) => { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("DISM"), summary: vec![DVLine { line_parts: vec![String::from("DISM: "), String::from("Error")], line_colors: vec![Color::Reset, Color::Red], }], raw: err, }); } TaskResult::Output(stdout, stderr, _) => { let output_text = if stderr.is_empty() { stdout.clone() } else { format!("{stdout}\n\n-------\n\n{stderr}") }; if output_text.contains("no component store corruption detected") { diag_group.passed.push(true); diag_group.result = String::from("OK"); diag_group.logs.push(Log { label: String::from("DISM"), summary: vec![DVLine { line_parts: vec![String::from("DISM: "), String::from("OK")], line_colors: vec![Color::Reset, Color::Green], }], raw: output_text, }); } else { diag_group.passed.push(false); diag_group.result = String::from("Unknown"); diag_group.logs.push(Log { label: String::from("DISM"), summary: vec![DVLine { line_parts: vec![String::from("DISM: "), String::from("Unknown")], line_colors: vec![Color::Reset, Color::Yellow], }], raw: output_text, }); } } } } pub fn parse_registry_hives( diag_group: &mut DiagGroup, task_result: TaskResult, cmd_args: Vec, ) { if !cfg!(windows) { sleep(Duration::from_millis(500)); return (); } let hive = cmd_args.get(2); match task_result { TaskResult::Error(err) => { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("Registry"), summary: if hive.is_some() { vec![DVLine { line_parts: vec![ format!("Registry ({}): ", hive.unwrap()), String::from("Error"), ], line_colors: vec![Color::Reset, Color::Red], }] } else { Vec::new() }, raw: err, }); } TaskResult::Output(stdout, stderr, passed) => { let output_text = if stderr.is_empty() { stdout.clone() } else { format!("{stdout}\n\n{stderr}") }; if passed { diag_group.passed.push(true); diag_group.result = String::from("OK"); diag_group.logs.push(Log { label: String::from("Registry"), summary: if hive.is_some() { vec![DVLine { line_parts: vec![ format!("Registry ({}): ", hive.unwrap()), String::from("OK"), ], line_colors: vec![Color::Reset, Color::Green], }] } else { Vec::new() }, raw: output_text, }); } else { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("Registry"), summary: if hive.is_some() { vec![DVLine { line_parts: vec![ format!("Registry ({}): ", hive.unwrap()), String::from("Error"), ], line_colors: vec![Color::Reset, Color::Red], }] } else { Vec::new() }, raw: output_text, }); } } } } pub fn parse_system_files(diag_group: &mut DiagGroup, task_result: TaskResult) { if !cfg!(windows) { sleep(Duration::from_millis(500)); return (); } match task_result { TaskResult::Error(err) => { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("System Files"), summary: vec![DVLine { line_parts: vec![String::from("System Files: "), String::from("Error")], line_colors: vec![Color::Reset, Color::Red], }], raw: err, }); } TaskResult::Output(stdout, stderr, passed) => { let output_text = if stderr.is_empty() { stdout.clone() } else { format!("{stdout}\n\n{stderr}") }; if passed { diag_group.passed.push(true); diag_group.result = String::from("OK"); diag_group.logs.push(Log { label: String::from("System Files"), summary: vec![DVLine { line_parts: vec![String::from("System Files: "), String::from("OK")], line_colors: vec![Color::Reset, Color::Green], }], raw: output_text, }); } else { diag_group.passed.push(false); diag_group.result = String::from("Error"); diag_group.logs.push(Log { label: String::from("System Files"), summary: vec![DVLine { line_parts: vec![String::from("System Files: "), String::from("Error")], line_colors: vec![Color::Reset, Color::Red], }], raw: output_text, }); } } } } pub fn remove_carriage_returns(text: &str) -> String { let re_carriage_returns = REGEXES.carriage_returns(); let parsed_lines: Vec = text .split("\r\n") .filter_map(|line| { let line = re_carriage_returns.replace_all(line, "").trim().to_string(); if line.is_empty() { None } else { Some(line) } }) .collect(); parsed_lines.join("\n") }