A cbor+zstd log management tool (grep, extract, merge, split, repack, redact)
cbor zstd aperture logs
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192use super::*;use crate::builtin;use crate::filter::FilterArgs;use crate::format;use crate::mailbox;use crate::md_thread;use crate::redact;use crate::secrets;use crate::theme;use crate::thread::ThreadBuilder;use anyhow::{anyhow, Result};use std::path::{Path, PathBuf};
// ---------------------------------------------------------------------------// thread// ---------------------------------------------------------------------------
/// Reconstructs conversation threads from request message histories.////// Each record's `capture.requestBody.messages` echoes its full parent path, so/// the trie over message-content hashes recovers the branching structure.#[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)]pub enum ThreadFormat { Json, Html, Mbox, Maildir, Md,}
#[derive(clap::Args)]pub struct ThreadArgs { #[arg(required = true)] pub files: Vec<PathBuf>, /// Write JSON/HTML/MBOX/Maildir to this path instead of stdout (`-` for stdout). #[arg(short, long)] pub output: Option<PathBuf>, /// Output format: json (default), html (built-in renderer), mbox, maildir, md. #[arg(long, value_enum, default_value = "json")] pub format: ThreadFormat, /// Body rendering for mbox/maildir: plain, html (multipart/alternative), html-only. #[arg(long, value_name = "MODE", default_value = "html")] pub body: String, /// Dark mode for the built-in HTML renderer. #[arg(long)] pub dark: bool, /// Render HTML using an Adium `.AdiumMessageStyle` bundle instead of the /// built-in renderer. #[arg(long, value_name = "BUNDLE")] pub theme: Option<PathBuf>, /// Adium style variant to apply (e.g. "Dark"). #[arg(long)] pub variant: Option<String>, #[command(flatten)] pub redact: redact::RedactArgs, /// Suppress the un-redacted-secrets warning for HTML/Markdown output. /// The warning fires when output is emitted without `--redact*` and a /// likely secret (API key, bearer token, JWT, …) is detected. This flag /// acknowledges the risk; it does not disable detection for other paths. #[arg(long = "i-know", default_value_t = false)] pub i_know: bool, #[command(flatten)] pub filter: FilterArgs,}
pub fn cmd_thread(args: &ThreadArgs) -> Result<()> { let filter = args.filter.build()?; let redactor = args.redact.build()?; let do_redact = redactor.is_active();
// Secrets-safety net: when emitting human-readable output (HTML, Markdown) // without `--redact*`, scan the rendered bytes for likely-secret patterns // and warn on stderr. Skipped when redaction was applied (already scrubbed // for valid-UTF-8 bodies) or when the user passed `--i-know`. mbox/maildir // are not scanned here (their renderers stream to files/dirs without a // single in-memory buffer); the warning is a best-effort heuristic anyway. let warn_secrets = |bytes: &[u8]| { if do_redact || args.i_know { return; } if let Ok(text) = std::str::from_utf8(bytes) { if let Some(w) = secrets::scan(text).warning() { eprintln!("{w}"); } } };
let mut builder = ThreadBuilder::new(); let mut total = 0u64; let mut with_messages = 0u64; for_each_matching_record(&args.files, &filter, |mut rec| { if do_redact { // Scrub text and valid-UTF-8 byte bodies (e.g. raw HTTP) before // the thread builder extracts message content / metadata. format::redact_strings(&mut rec, &|s| redactor.apply(s)); } total += 1; if builder.add_record(&rec)? { with_messages += 1; } Ok(()) })?; let j = builder.to_json(total, with_messages);
// Built-in long-form HTML renderer (no external theme bundle). if args.format == ThreadFormat::Html && args.theme.is_none() { let html = builtin::render_html(&j, args.dark)?; warn_secrets(html.as_bytes()); write_output(args.output.as_ref(), html.as_bytes(), "html")?; eprintln!( "{}; built-in HTML{}", thread_summary(total, with_messages, &j), if args.dark { " (dark)" } else { "" } ); return Ok(()); }
// HTML (themed) path: render the forest through an Adium message style. if let Some(theme_path) = &args.theme { let html = theme::render_forest(&j, theme_path, args.variant.as_deref())?; warn_secrets(html.as_bytes()); write_output(args.output.as_ref(), html.as_bytes(), "html")?; eprintln!( "{}; themed -> {}", thread_summary(total, with_messages, &j), theme_path.display() ); return Ok(()); }
// MBOX / Maildir export: each trie node becomes one email, threaded by // Message-ID / In-Reply-To. Selected by --format mbox|maildir. if args.format == ThreadFormat::Mbox || args.format == ThreadFormat::Maildir { let mode = mailbox::BodyMode::parse(&args.body)?; if args.format == ThreadFormat::Mbox { // mbox may write to stdout (`-` or no -o) or a file. let n = match args.output.as_ref() { Some(p) if p.as_path() != Path::new("-") => mailbox::write_mbox(&j, mode, p)?, _ => { let mut out = std::io::stdout(); mailbox::write_mbox_to(&mut out, &j, mode)? } }; eprintln!( "{} record(s) ({} with messages) -> {} message(s) [{} body] (mbox)", total, with_messages, n, args.body, ); return Ok(()); } // maildir requires a real directory path. let out = args .output .as_ref() .filter(|p| p.as_path() != Path::new("-")) .ok_or_else(|| anyhow!("--format maildir requires -o DIR"))?; let n = mailbox::write_maildir(&j, mode, out)?; eprintln!( "{} record(s) ({} with messages) -> {} message(s) [{} body] -> {} (maildir)", total, with_messages, n, args.body, out.display() ); return Ok(()); }
if args.format == ThreadFormat::Md { let md = md_thread::render_md(&j); warn_secrets(md.as_bytes()); write_output(args.output.as_ref(), md.as_bytes(), "md")?; eprintln!("{}; markdown", thread_summary(total, with_messages, &j)); return Ok(()); }
let pretty = serde_json::to_string_pretty(&j)?; let bytes = pretty.into_bytes(); write_output(args.output.as_ref(), &bytes, "json")?; eprintln!("{}", thread_summary(total, with_messages, &j)); Ok(())}
/// Common summary line for `cmd_thread`: records scanned, how many carried/// messages, and the reconstructed thread / branch-point counts. Used by the/// built-in HTML, themed, and JSON output paths (each appends its own suffix).fn thread_summary(total: u64, with_messages: u64, j: &serde_json::Value) -> String { format!( "{} record(s) ({} with messages) -> {} thread(s), {} branch point(s)", total, with_messages, j["root_count"], j["branch_count"], )}