diff options
Diffstat (limited to 'src/bin/mft_dump.rs')
-rw-r--r-- | src/bin/mft_dump.rs | 376 |
1 files changed, 324 insertions, 52 deletions
diff --git a/src/bin/mft_dump.rs b/src/bin/mft_dump.rs index fdb1f26..aff2cb0 100644 --- a/src/bin/mft_dump.rs +++ b/src/bin/mft_dump.rs @@ -1,17 +1,34 @@ use clap::{App, Arg, ArgMatches}; use env_logger; -use log::info; +use indoc::indoc; +use log::{info, Level}; +use mft::attribute::{MftAttributeContent, MftAttributeType}; use mft::mft::MftParser; use mft::{MftEntry, ReadSeek}; +use dialoguer::Confirmation; use mft::csv::FlatMftEntryWithName; -use std::io; + +use snafu::{Backtrace, ErrorCompat, Snafu}; +use std::fs::File; use std::io::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::process::exit; + +use std::{fs, io, path}; + +/// Simple error macro for use inside of internal errors in `EvtxDump` +macro_rules! err { + ($($tt:tt)*) => { Err(Box::<dyn std::error::Error>::from(format!($($tt)*))) } +} + +type StdErr = Box<dyn std::error::Error>; +#[derive(Debug, PartialOrd, PartialEq)] enum OutputFormat { JSON, + JSONL, CSV, } @@ -19,6 +36,7 @@ impl OutputFormat { pub fn from_str(s: &str) -> Option<Self> { match s { "json" => Some(OutputFormat::JSON), + "jsonl" => Some(OutputFormat::JSONL), "csv" => Some(OutputFormat::CSV), _ => None, } @@ -27,87 +45,295 @@ impl OutputFormat { struct MftDump { filepath: PathBuf, - indent: bool, + // We use an option here to be able to move the output out of mftdump from a mutable reference. + output: Option<Box<dyn Write>>, + data_streams_output: Option<PathBuf>, + verbosity_level: Option<Level>, output_format: OutputFormat, + backtraces: bool, } impl MftDump { - pub fn from_cli_matches(matches: &ArgMatches) -> Self { - MftDump { + pub fn from_cli_matches(matches: &ArgMatches) -> Result<Self, StdErr> { + let output_format = + OutputFormat::from_str(matches.value_of("output-format").unwrap_or_default()) + .expect("Validated with clap default values"); + + let backtraces = matches.is_present("backtraces"); + + let output: Option<Box<dyn Write>> = if let Some(path) = matches.value_of("output-target") { + match Self::create_output_file(path, !matches.is_present("no-confirm-overwrite")) { + Ok(f) => Some(Box::new(f)), + Err(e) => { + return err!( + "An error occurred while creating output file at `{}` - `{}`", + path, + e + ); + } + } + } else { + Some(Box::new(io::stdout())) + }; + + let data_streams_output = if let Some(path) = matches.value_of("data-streams-target") { + let path = PathBuf::from(path); + Self::create_output_dir(&path)?; + Some(path) + } else { + None + }; + + let verbosity_level = match matches.occurrences_of("verbose") { + 0 => None, + 1 => Some(Level::Info), + 2 => Some(Level::Debug), + 3 => Some(Level::Trace), + _ => { + eprintln!("using more than -vvv does not affect verbosity level"); + Some(Level::Trace) + } + }; + + Ok(MftDump { filepath: PathBuf::from(matches.value_of("INPUT").expect("Required argument")), - indent: !matches.is_present("no-indent"), - output_format: OutputFormat::from_str( - matches.value_of("output-format").unwrap_or_default(), - ) - .expect("Validated with clap default values"), - } + output, + data_streams_output, + verbosity_level, + output_format, + backtraces, + }) } - pub fn print_json_entry(&self, entry: &MftEntry) { - let json_str = if self.indent { - serde_json::to_string_pretty(&entry).expect("It should be valid UTF-8") + fn create_output_dir(path: impl AsRef<Path>) -> Result<(), StdErr> { + let p = path.as_ref(); + + if p.exists() { + if !p.is_dir() { + return err!("There is a file at {}, refusing to overwrite", p.display()); + } + // p exists and is a directory, it's ok to add files. } else { - serde_json::to_string(&entry).expect("It should be valid UTF-8") - }; + fs::create_dir_all(path)? + } - println!("{}", json_str); + Ok(()) } - pub fn print_csv_entry<W: Write>( - &self, - entry: &MftEntry, - parser: &mut MftParser<impl ReadSeek>, - writer: &mut csv::Writer<W>, - ) { - let flat_entry = FlatMftEntryWithName::from_entry(&entry, parser); + /// If `prompt` is passed, will display a confirmation prompt before overwriting files. + fn create_output_file( + path: impl AsRef<Path>, + prompt: bool, + ) -> Result<File, Box<dyn std::error::Error>> { + let p = path.as_ref(); - writer.serialize(flat_entry).expect("Writing to CSV failed"); + if p.is_dir() { + return err!( + "There is a directory at {}, refusing to overwrite", + p.display() + ); + } + + if p.exists() { + if prompt { + match Confirmation::new() + .with_text(&format!( + "Are you sure you want to override output file at {}", + p.display() + )) + .default(false) + .interact() + { + Ok(true) => Ok(File::create(p)?), + Ok(false) => err!("Cancelled"), + Err(e) => err!( + "Failed to write confirmation prompt to term caused by\n{}", + e + ), + } + } else { + Ok(File::create(p)?) + } + } else { + // Ok to assume p is not an existing directory + match p.parent() { + Some(parent) => + // Parent exist + { + if parent.exists() { + Ok(File::create(p)?) + } else { + fs::create_dir_all(parent)?; + Ok(File::create(p)?) + } + } + None => err!("Output file cannot be root."), + } + } } - pub fn parse_file(&self) { - info!("Opening file {:?}", &self.filepath); - let mut mft_handler = match MftParser::from_path(&self.filepath) { - Ok(mft_handler) => mft_handler, - Err(error) => { - eprintln!( - "Failed to parse {:?}, failed with: [{}]", - &self.filepath, error - ); - std::process::exit(-1); + /// Main entry point for `EvtxDump` + pub fn run(&mut self) -> Result<(), StdErr> { + self.try_to_initialize_logging(); + + let mut parser = match MftParser::from_path(&self.filepath) { + Ok(parser) => parser, + Err(e) => { + return err!( + "Failed to open file {}.\n\tcaused by: {}", + self.filepath.display(), + &e + ) } }; + // Since the JSON parser can do away with a &mut Write, but the csv parser needs ownership + // of `Write`, we eagerly create the csv writer here, moving the Box<Write> out from + // `Mftdump` and replacing it with None placeholder. let mut csv_writer = match self.output_format { - OutputFormat::CSV => Some(csv::Writer::from_writer(io::stdout())), + OutputFormat::CSV => { + Some(csv::Writer::from_writer(self.output.take().expect( + "There can only be one flow accessing the output at a time", + ))) + } _ => None, }; - let number_of_entries = mft_handler.get_entry_count(); + let number_of_entries = parser.get_entry_count(); for i in 0..number_of_entries { - let entry = mft_handler.get_entry(i); + let entry = parser.get_entry(i); let entry = match entry { Ok(entry) => entry, Err(error) => { - eprintln!("Failed to parse MFT entry {}, failed with: [{}]", i, error); + eprintln!("{}", error); + + if self.backtraces { + if let Some(bt) = error.backtrace() { + eprintln!("{}", bt); + } + } continue; } }; + if let Some(data_streams_dir) = &self.data_streams_output { + if let Ok(Some(path)) = parser.get_full_path_for_entry(&entry) { + let sanitized_path = sanitized(&path.to_string_lossy().to_string()); + + for (i, (name, stream)) in entry + .iter_attributes() + .filter_map(|a| a.ok()) + .filter_map(|a| { + if a.header.type_code == MftAttributeType::DATA { + // resident + let name = a.header.name.clone(); + if let Some(data) = a.data.into_data() { + Some((name, data)) + } else { + None + } + } else { + None + } + }) + .enumerate() + { + let orig_path_component: String = data_streams_dir + .join(&sanitized_path) + .to_string_lossy() + .to_string() + .chars() + .take(150) + .collect(); + + let data_stream_path = format!( + "{parent_name}__{stream_number}_{stream_name}.dontrun", + parent_name = orig_path_component, + stream_number = i, + stream_name = name + ); + + let mut f = File::create(&data_stream_path)?; + f.write_all(stream.data())?; + } + } + } + match self.output_format { - OutputFormat::JSON => self.print_json_entry(&entry), + OutputFormat::JSON | OutputFormat::JSONL => self.print_json_entry(&entry)?, OutputFormat::CSV => self.print_csv_entry( &entry, - &mut mft_handler, + &mut parser, csv_writer .as_mut() .expect("CSV Writer is for OutputFormat::CSV"), - ), + )?, } } + + Ok(()) + } + + fn try_to_initialize_logging(&self) { + if let Some(level) = self.verbosity_level { + match simplelog::WriteLogger::init( + level.to_level_filter(), + simplelog::Config::default(), + io::stderr(), + ) { + Ok(_) => {} + Err(e) => eprintln!("Failed to initialize logging: {:?}", e), + }; + } + } + + pub fn print_json_entry(&mut self, entry: &MftEntry) -> Result<(), Box<dyn std::error::Error>> { + let out = self + .output + .as_mut() + .expect("CSV Flow cannot occur, so `Mftdump` should still Own `output`"); + + let json_str = if self.output_format == OutputFormat::JSON { + serde_json::to_vec_pretty(&entry).expect("It should be valid UTF-8") + } else { + serde_json::to_vec(&entry).expect("It should be valid UTF-8") + }; + + out.write_all(&json_str)?; + out.write_all(b"\n")?; + + Ok(()) + } + + pub fn print_csv_entry<W: Write>( + &self, + entry: &MftEntry, + parser: &mut MftParser<impl ReadSeek>, + writer: &mut csv::Writer<W>, + ) -> Result<(), Box<dyn std::error::Error>> { + let flat_entry = FlatMftEntryWithName::from_entry(&entry, parser); + + writer.serialize(flat_entry)?; + + Ok(()) } } +// adapter from python version +// https://github.com/pallets/werkzeug/blob/9394af646038abf8b59d6f866a1ea5189f6d46b8/src/werkzeug/utils.py#L414 +pub fn sanitized(component: &str) -> String { + let mut buf = String::with_capacity(component.len()); + for c in component.chars() { + match c { + _sep if path::is_separator(c) => buf.push('_'), + _ => buf.push(c), + } + } + + buf +} + fn main() { env_logger::init(); @@ -117,22 +343,68 @@ fn main() { .about("Utility for parsing MFT snapshots") .arg(Arg::with_name("INPUT").required(true)) .arg( - Arg::with_name("no-indent") - .long("--no-indent") - .takes_value(false) - .help("When set, output will not be indented (works only with JSON output)."), - ) - .arg( Arg::with_name("output-format") .short("-o") .long("--output-format") .takes_value(true) - .possible_values(&["csv", "json"]) + .possible_values(&["csv", "json", "jsonl"]) .default_value("json") .help("Output format."), ) + .arg( + Arg::with_name("output-target") + .long("--output") + .short("-f") + .takes_value(true) + .help(indoc!("Writes output to the file specified instead of stdout, errors will still be printed to stderr. + Will ask for confirmation before overwriting files, to allow overwriting, pass `--no-confirm-overwrite` + Will create parent directories if needed.")), + ) + .arg( + Arg::with_name("data-streams-target") + .long("--extract-resident-streams") + .short("-e") + .takes_value(true) + .help(indoc!("Writes resident data streams to the given directory, resident streams will be named\ + <original_file_path>_<stream_number>/<stream_name>.norun")), + ) + .arg( + Arg::with_name("no-confirm-overwrite") + .long("--no-confirm-overwrite") + .takes_value(false) + .help(indoc!("When set, will not ask for confirmation before overwriting files, useful for automation")), + ) + .arg(Arg::with_name("verbose") + .short("-v") + .multiple(true) + .takes_value(false) + .help(indoc!(r#" + Sets debug prints level for the application: + -v - info + -vv - debug + -vvv - trace + NOTE: trace output is only available in debug builds, as it is extremely verbose."#)) + ) + .arg( + Arg::with_name("backtraces") + .long("--backtraces") + .takes_value(false) + .help("If set, a backtrace will be printed with some errors if available")) .get_matches(); - let app = MftDump::from_cli_matches(&matches); - app.parse_file(); + let mut app = match MftDump::from_cli_matches(&matches) { + Ok(app) => app, + Err(e) => { + eprintln!("An error occurred while setting up the app: {}", &e); + exit(1); + } + }; + + match app.run() { + Ok(()) => {} + Err(e) => { + eprintln!("A runtime error has occurred {}", &e); + exit(1); + } + }; } |