deja-vu/boot_diags/src/diags.rs

396 lines
14 KiB
Rust

// 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 <https://www.gnu.org/licenses/>.
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<Regex>,
bitlocker_off: OnceLock<Regex>,
carriage_returns: OnceLock<Regex>,
chkdsk_splits: OnceLock<Regex>,
}
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<DVLine>,
pub raw: String,
}
#[derive(Clone, Debug)]
pub struct DiagGroup {
pub diag_type: Type,
pub passed: Vec<bool>,
pub logs: Vec<Log>,
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<String> = 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<DVLine> {
let mut summaries: Vec<DVLine> = 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_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_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<String> = 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")
}