use anyhow::{Context, Result};
use clap::Parser;
use ese_parser::EseFile;
#[derive(Parser)]
pub(crate) struct NtdsOfflineArgs {
#[arg(long, value_name = "PATH")]
pub ntds: String,
#[arg(long, value_name = "PATH")]
pub system: String,
#[arg(long)]
pub json: bool,
}
pub(crate) async fn ntds_offline(a: NtdsOfflineArgs) -> Result<()> {
let sp = crate::ui::Spinner::start(format!("ESE header validate → {}", a.ntds));
let f = EseFile::open(&a.ntds).with_context(|| format!("open ESE file {}", a.ntds))?;
let fmt_ver = f.format_version();
let page = f.page_size();
let file_type = format!("{:?}", f.file_type());
let sys_meta =
std::fs::metadata(&a.system).with_context(|| format!("stat --system {}", a.system))?;
let sys_size = sys_meta.len();
sp.done(&format!(
"{}: ESE format v{} · {}-byte page · type={} · SYSTEM hive {} bytes",
a.ntds, fmt_ver, page, file_type, sys_size
));
eprintln!(
"[hint] this step is a known adhammer gap: NTDS.dit ESE row walk lands \
upstream at ese-parser 0.2. adhammer-secrets already carries bootkey \
+ PEK crypto; the last mile is the datatable B-tree walk."
);
eprintln!(
"[hint] external tool: impacket-secretsdump -ntds {ntds} -system {sys} LOCAL",
ntds = a.ntds,
sys = a.system
);
eprintln!("[hint] see docs/GAPS.md#ntds-dit-offline for the full row");
if a.json {
println!(
"{{\"command\":\"adhammer attack ntds-offline\",\"success\":true,\
\"evidence\":{{\"ntds\":{n:?},\"system\":{s:?},\
\"ese_format\":{fv},\"page_size\":{ps},\"file_type\":{ft:?},\
\"system_hive_bytes\":{ss},\
\"status\":\"validated; row walk deferred to ese-parser 0.2\"}}}}",
n = a.ntds,
s = a.system,
fv = fmt_ver,
ps = page,
ft = file_type,
ss = sys_size
);
}
Ok(())
}