//! Read a raw capture and emit a repository-safe, replayable fixture. //! //! ```text //! blaze-sanitize [-o out.jsonl] [--report report.txt] //! ``` //! //! Prints an audit of exactly what was replaced. Nothing is redacted silently: //! every substitution appears in the report with its path, kind and length, and //! untouched frames keep their exact wire bytes. //! //! The output is a JSONL conversation that replays against either backend — //! see `blaze-probe --capture`. use std::io::Write; use openfut_blaze_host::capture::{self, Direction}; use openfut_blaze_host::sanitize::{self, SENSITIVE_TAGS}; use openfut_protocol_blaze::fire2::Header; fn usage() -> ! { eprintln!("usage: blaze-sanitize [-o out.jsonl] [--report report.txt]"); eprintln!(); eprintln!("Reads a raw frame capture and writes a sanitized, replayable JSONL"); eprintln!("conversation plus an audit of every redaction made."); std::process::exit(2); } fn arg_after(args: &[String], flag: &str) -> Option { args.windows(2).find(|w| w[0] == flag).map(|w| w[1].clone()) } fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } fn json_escape(s: &str) -> String { s.chars() .flat_map(|c| match c { '"' => "\\\"".chars().collect::>(), '\\' => "\\\\".chars().collect(), '\n' => "\\n".chars().collect(), c => vec![c], }) .collect() } fn main() { let args: Vec = std::env::args().skip(1).collect(); if args.is_empty() || args[0].starts_with('-') { usage(); } let input = args[0].clone(); let out_path = arg_after(&args, "-o").unwrap_or_else(|| format!("{input}.sanitized.jsonl")); let report_path = arg_after(&args, "--report"); let records = match capture::read_file(&input) { Ok(r) => r, Err(e) => { eprintln!("blaze-sanitize: cannot read {input}: {e}"); std::process::exit(1); } }; eprintln!("read {} frame(s) from {input}", records.len()); let (out, report) = match sanitize::sanitize(&records) { Ok(v) => v, Err(e) => { eprintln!("blaze-sanitize: {e}"); std::process::exit(1); } }; // ------------------------------------------------------------- fixture let mut f = match std::fs::File::create(&out_path) { Ok(f) => f, Err(e) => { eprintln!("blaze-sanitize: cannot write {out_path}: {e}"); std::process::exit(1); } }; let _ = writeln!( f, "{{\"kind\":\"capture\",\"source\":\"{}\",\"frames\":{},\"redacted_frames\":{},\ \"sensitive_tags\":[{}],\"note\":\"sanitized; redacted values are \ length-preserving fillers, all other bytes are exact\"}}", json_escape(&input), report.frames_out, report.frames_redacted, SENSITIVE_TAGS .iter() .map(|t| format!("\"{t}\"")) .collect::>() .join(",") ); for (rec, bytes) in &out { let h = Header::parse(bytes).ok(); let (comp, cmd, mtype, mnum, uidx, plen) = match h { Some(h) => ( h.component, h.command, h.msg_type.as_bits(), h.msg_num, h.user_index, h.payload_len, ), None => (0, 0, 0, 0, 0, 0), }; let _ = writeln!( f, "{{\"kind\":\"frame\",\"conn\":{},\"seq\":{},\"dir\":\"{}\",\ \"component\":{},\"command\":{},\"msg_type\":{},\"msg_num\":{},\ \"user_index\":{},\"payload_len\":{},\"frame_hex\":\"{}\"}}", rec.conn_id, rec.seq, rec.dir.label(), comp, cmd, mtype, mnum, uidx, plen, hex(bytes) ); } eprintln!("wrote {out_path}"); // -------------------------------------------------------------- report let mut lines = Vec::new(); lines.push(format!("sanitization report for {input}")); lines.push(format!(" frames in: {}", report.frames_in)); lines.push(format!(" frames out: {}", report.frames_out)); lines.push(format!(" frames redacted: {}", report.frames_redacted)); lines.push(format!(" redactions: {}", report.redactions.len())); lines.push(format!(" sensitive tags: {}", SENSITIVE_TAGS.join(", "))); if !report.undecodable.is_empty() { lines.push(format!( " NOT decodable as TDF (passed through unchanged, NOT inspected): seq {:?}", report.undecodable )); } lines.push(String::new()); if report.redactions.is_empty() { lines.push(" no sensitive values found".into()); } else { lines.push(" seq conn path kind bytes".into()); for r in &report.redactions { lines.push(format!( " {:<5} {:<5} {:<20} {:<7} {} -> {}", r.seq, r.conn_id, r.path, r.kind, r.original_len, r.replacement_len )); } } // Direction/route summary, so a reviewer can see the conversation shape. lines.push(String::new()); lines.push(" conversation shape:".into()); let rx = out.iter().filter(|(r, _)| r.dir == Direction::Rx).count(); let tx = out.len() - rx; lines.push(format!(" {rx} RX, {tx} TX")); let text = lines.join("\n") + "\n"; print!("{text}"); if let Some(p) = report_path { if std::fs::write(&p, &text).is_ok() { eprintln!("wrote {p}"); } } }