Skip to main content

rusthound_ce/json/maker/
common.rs

1use obfstr::obfstr;
2
3use serde::Serialize;
4
5use colored::Colorize;
6use log::{info, debug, trace};
7use rayon::prelude::*;
8
9use std::fs;
10use std::fs::File;
11use std::io::{BufWriter, Write};
12use std::error::Error;
13
14use zip::write::{SimpleFileOptions, ZipWriter};
15
16extern crate zip;
17use crate::args::{Options, RUSTHOUND_VERSION};
18use crate::objects::common::{Meta, LdapObject};
19
20/// Current Bloodhound version 4.3+
21pub const BLOODHOUND_VERSION_4: i8 = 6;
22
23/// Number of objects serialized per parallel batch.
24///
25/// Objects in a batch are serialized to JSON concurrently (using all CPU
26/// cores) then flushed sequentially to the output, so peak extra memory is
27/// bounded by one batch instead of the whole dataset.
28const SERIALIZE_BATCH: usize = 4096;
29
30/// Where a JSON collection is written: either straight into an entry of the
31/// shared zip archive, or into its own `.json` file on disk.
32enum Sink<'a> {
33    Zip { writer: &'a mut ZipWriter<BufWriter<File>>, datetime: &'a str, domain: &'a str },
34    Dir { path: &'a str, datetime: &'a str, domain: &'a str },
35}
36
37/// Stream one object collection as a BloodHound JSON document
38/// (`{"data":[...],"meta":{...}}`) into `writer`, without ever materialising
39/// the whole document (or a `serde_json::Value` DOM) in memory.
40///
41/// Objects are serialized in parallel batches, which is both faster on
42/// many-core machines and keeps memory flat regardless of object count.
43fn write_json_document<T, W>(writer: &mut W, name: &str, vec_json: &[T]) -> std::io::Result<()>
44where
45    T: LdapObject + Serialize + Sync,
46    W: Write,
47{
48    let count = vec_json.len();
49
50    writer.write_all(b"{\"data\":[")?;
51
52    let mut first = true;
53    for batch in vec_json.chunks(SERIALIZE_BATCH) {
54        // Serialize every object of this batch concurrently across all cores.
55        let parts: Vec<String> = batch
56            .par_iter()
57            .map(|object| serde_json::to_string(object).expect("object serialization failed"))
58            .collect();
59
60        for part in parts {
61            if !first {
62                writer.write_all(b",")?;
63            }
64            first = false;
65            writer.write_all(part.as_bytes())?;
66        }
67    }
68
69    let meta = Meta::new(
70        0_i32,
71        name.to_owned(),
72        count as i32,
73        BLOODHOUND_VERSION_4,
74        format!("RustHound-CE v{}", RUSTHOUND_VERSION.to_owned()),
75    );
76    writer.write_all(b"],\"meta\":")?;
77    serde_json::to_writer(&mut *writer, &meta)?;
78    writer.write_all(b"}")?;
79
80    Ok(())
81}
82
83/// Emit one object collection to the selected [`Sink`]. Empty collections are
84/// skipped, matching the previous behaviour (no file/entry is created).
85fn emit<T>(sink: &mut Sink, name: &str, vec_json: &[T]) -> Result<(), Box<dyn Error>>
86where
87    T: LdapObject + Serialize + Sync,
88{
89    if vec_json.is_empty() {
90        return Ok(());
91    }
92
93    let count = vec_json.len();
94    debug!("Making {name}.json");
95
96    match sink {
97        Sink::Zip { writer, datetime, domain } => {
98            // `large_file(true)` enables ZIP64, which is required for any entry
99            // larger than 4 GiB. Without it the zip crate errors out on huge
100            // domains, which is what caused the crash on large outputs.
101            let options = SimpleFileOptions::default()
102                .compression_method(zip::CompressionMethod::Deflated)
103                .large_file(true);
104            // Keep the original entry naming scheme: <datetime>_<domain>_<name>.json
105            let filename = format!("{datetime}_{domain}_{name}.json");
106            trace!("Adding file {}", filename.bold());
107            writer.start_file(&filename, options)?;
108            write_json_document(*writer, name, vec_json)?;
109        }
110        Sink::Dir { path, datetime, domain } => {
111            let final_path = format!("{path}/{datetime}_{domain}_{name}.json");
112            let file = File::create(&final_path)?;
113            let mut buf = BufWriter::new(file);
114            write_json_document(&mut buf, name, vec_json)?;
115            buf.flush()?;
116            info!("{} created!", final_path.bold());
117        }
118    }
119
120    info!("{} {name} parsed!", count.to_string().bold());
121    Ok(())
122}
123
124/// Emit every object collection contained in `ad_results` into the given sink.
125macro_rules! emit_all {
126    ($sink:expr, $ad:expr) => {{
127        emit($sink, "users", &$ad.users)?;
128        emit($sink, "groups", &$ad.groups)?;
129        emit($sink, "computers", &$ad.computers)?;
130        emit($sink, "ous", &$ad.ous)?;
131        emit($sink, "domains", &$ad.domains)?;
132        emit($sink, "gpos", &$ad.gpos)?;
133        emit($sink, "containers", &$ad.containers)?;
134        emit($sink, "ntauthstores", &$ad.ntauthstores)?;
135        emit($sink, "aiacas", &$ad.aiacas)?;
136        emit($sink, "rootcas", &$ad.rootcas)?;
137        emit($sink, "enterprisecas", &$ad.enterprisecas)?;
138        emit($sink, "certtemplates", &$ad.certtemplates)?;
139        emit($sink, "issuancepolicies", &$ad.issuancepolicies)?;
140    }};
141}
142
143/// Write all collections as individual `.json` files on disk.
144pub fn write_json_files(
145    datetime: &str,
146    domain_format: &str,
147    common_args: &Options,
148    ad_results: &crate::api::ADResults,
149) -> Result<(), Box<dyn Error>> {
150    fs::create_dir_all(&common_args.path)?;
151    let mut sink = Sink::Dir {
152        path: &common_args.path,
153        datetime,
154        domain: domain_format,
155    };
156    emit_all!(&mut sink, ad_results);
157    Ok(())
158}
159
160/// Stream all collections into a single zip archive.
161pub fn make_a_zip(
162    datetime: &str,
163    domain: &str,
164    path: &str,
165    ad_results: &crate::api::ADResults,
166) -> Result<String, Box<dyn Error>> {
167    fs::create_dir_all(path)?;
168    let final_path = format!("{path}/{datetime}_{domain}_{}.zip", obfstr!("rusthound-ce"));
169
170    let file = File::create(&final_path)?;
171    // A large buffer keeps the single-threaded deflate stream fed and cuts
172    // syscall overhead on multi-GB archives.
173    let mut writer = ZipWriter::new(BufWriter::with_capacity(1 << 20, file));
174
175    trace!("Making the ZIP file");
176    {
177        let mut sink = Sink::Zip { writer: &mut writer, datetime, domain };
178        emit_all!(&mut sink, ad_results);
179    }
180    writer.finish()?.flush()?;
181
182    info!("{} created!", &final_path.bold());
183    Ok(final_path)
184}