use obfstr::obfstr;
use serde::Serialize;
use colored::Colorize;
use log::{info, debug, trace};
use rayon::prelude::*;
use std::fs;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::error::Error;
use zip::write::{SimpleFileOptions, ZipWriter};
extern crate zip;
use crate::args::{Options, RUSTHOUND_VERSION};
use crate::objects::common::{Meta, LdapObject};
pub const BLOODHOUND_VERSION_4: i8 = 6;
const SERIALIZE_BATCH: usize = 4096;
enum Sink<'a> {
Zip { writer: &'a mut ZipWriter<BufWriter<File>>, datetime: &'a str, domain: &'a str },
Dir { path: &'a str, datetime: &'a str, domain: &'a str },
}
fn write_json_document<T, W>(writer: &mut W, name: &str, vec_json: &[T]) -> std::io::Result<()>
where
T: LdapObject + Serialize + Sync,
W: Write,
{
let count = vec_json.len();
writer.write_all(b"{\"data\":[")?;
let mut first = true;
for batch in vec_json.chunks(SERIALIZE_BATCH) {
let parts: Vec<String> = batch
.par_iter()
.map(|object| serde_json::to_string(object).expect("object serialization failed"))
.collect();
for part in parts {
if !first {
writer.write_all(b",")?;
}
first = false;
writer.write_all(part.as_bytes())?;
}
}
let meta = Meta::new(
0_i32,
name.to_owned(),
count as i32,
BLOODHOUND_VERSION_4,
format!("RustHound-CE v{}", RUSTHOUND_VERSION.to_owned()),
);
writer.write_all(b"],\"meta\":")?;
serde_json::to_writer(&mut *writer, &meta)?;
writer.write_all(b"}")?;
Ok(())
}
fn emit<T>(sink: &mut Sink, name: &str, vec_json: &[T]) -> Result<(), Box<dyn Error>>
where
T: LdapObject + Serialize + Sync,
{
if vec_json.is_empty() {
return Ok(());
}
let count = vec_json.len();
debug!("Making {name}.json");
match sink {
Sink::Zip { writer, datetime, domain } => {
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.large_file(true);
let filename = format!("{datetime}_{domain}_{name}.json");
trace!("Adding file {}", filename.bold());
writer.start_file(&filename, options)?;
write_json_document(*writer, name, vec_json)?;
}
Sink::Dir { path, datetime, domain } => {
let final_path = format!("{path}/{datetime}_{domain}_{name}.json");
let file = File::create(&final_path)?;
let mut buf = BufWriter::new(file);
write_json_document(&mut buf, name, vec_json)?;
buf.flush()?;
info!("{} created!", final_path.bold());
}
}
info!("{} {name} parsed!", count.to_string().bold());
Ok(())
}
macro_rules! emit_all {
($sink:expr, $ad:expr) => {{
emit($sink, "users", &$ad.users)?;
emit($sink, "groups", &$ad.groups)?;
emit($sink, "computers", &$ad.computers)?;
emit($sink, "ous", &$ad.ous)?;
emit($sink, "domains", &$ad.domains)?;
emit($sink, "gpos", &$ad.gpos)?;
emit($sink, "containers", &$ad.containers)?;
emit($sink, "ntauthstores", &$ad.ntauthstores)?;
emit($sink, "aiacas", &$ad.aiacas)?;
emit($sink, "rootcas", &$ad.rootcas)?;
emit($sink, "enterprisecas", &$ad.enterprisecas)?;
emit($sink, "certtemplates", &$ad.certtemplates)?;
emit($sink, "issuancepolicies", &$ad.issuancepolicies)?;
}};
}
pub fn write_json_files(
datetime: &str,
domain_format: &str,
common_args: &Options,
ad_results: &crate::api::ADResults,
) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(&common_args.path)?;
let mut sink = Sink::Dir {
path: &common_args.path,
datetime,
domain: domain_format,
};
emit_all!(&mut sink, ad_results);
Ok(())
}
pub fn make_a_zip(
datetime: &str,
domain: &str,
path: &str,
ad_results: &crate::api::ADResults,
) -> Result<String, Box<dyn Error>> {
fs::create_dir_all(path)?;
let final_path = format!("{path}/{datetime}_{domain}_{}.zip", obfstr!("rusthound-ce"));
let file = File::create(&final_path)?;
let mut writer = ZipWriter::new(BufWriter::with_capacity(1 << 20, file));
trace!("Making the ZIP file");
{
let mut sink = Sink::Zip { writer: &mut writer, datetime, domain };
emit_all!(&mut sink, ad_results);
}
writer.finish()?.flush()?;
info!("{} created!", &final_path.bold());
Ok(final_path)
}