use std::io::{Read, Write};
use std::process::{Command, Stdio};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use zeroize::{Zeroize, Zeroizing};
use crate::error::Error;
use crate::sandbox::SandboxTier;
use super::context::{prepare_execution, PreparedExecution};
pub struct SupervisedResult {
pub exit_code: i32,
pub leak_events: usize,
pub events: Vec<DataflowEvent>,
}
#[derive(Debug, Clone)]
pub struct DataflowEvent {
pub kind: DataflowEventKind,
pub timestamp_ms: u64,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataflowEventKind {
SecretInjected,
LeakedStdout,
LeakedStderr,
ProcessExited,
}
#[allow(clippy::too_many_lines)]
pub fn supervised_execute(
vault: &crate::vault::Vault,
secret_name: &str,
env_var: &str,
command: &[String],
tier: SandboxTier,
) -> Result<SupervisedResult, Error> {
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
if tier.any_isolation() {
return Err(Error::CryptoFailure(format!(
"supervised mode with sandbox tier '{}' is not supported on this platform",
tier.as_str()
)));
}
const MAX_SUPERVISABLE_SECRET_LEN: usize = 16383;
let mappings = [(secret_name, env_var)];
let prepared = prepare_execution(vault, &mappings, command)?;
if let Some((_, ref val)) = prepared.env_pairs.first() {
if val.len() > MAX_SUPERVISABLE_SECRET_LEN {
return Err(Error::CryptoFailure(format!(
"secret '{secret_name}' is {len} bytes — supervised mode cannot reliably \
detect leaks for secrets longer than {max} bytes. Use inject mode, split \
the secret, or store a shorter value.",
len = val.len(),
max = MAX_SUPERVISABLE_SECRET_LEN
)));
}
if val.is_empty() {
return Err(Error::CryptoFailure(format!(
"secret '{secret_name}' has an empty value — supervised mode cannot detect \
leaks of an empty string. Store a non-empty secret or use inject mode."
)));
}
}
let start_time = std::time::Instant::now();
let mut events: Vec<DataflowEvent> = Vec::new();
events.push(DataflowEvent {
kind: DataflowEventKind::SecretInjected,
timestamp_ms: 0,
detail: format!(
"secret '{secret_name}' injected as ${env_var} into {}",
command[0]
),
});
let mut cmd = build_supervised_command(&prepared, tier);
#[cfg(windows)]
let (mut child, _job_handle) = crate::sandbox::windows::spawn_in_job(&mut cmd, tier)
.map_err(|e| Error::CryptoFailure(format!("windows sandbox spawn failed: {e}")))?;
#[cfg(not(windows))]
let mut child = cmd.spawn().map_err(Error::ExecFailed)?;
let child_stdout = child.stdout.take();
let child_stderr = child.stderr.take();
let secret_bytes = match prepared.env_pairs.first() {
Some((_, val)) => val.as_bytes().to_vec(),
None => Vec::new(),
};
let secret_for_stdout = Zeroizing::new(secret_bytes.clone());
let stdout_start = start_time;
let stdout_handle = std::thread::spawn(move || -> (usize, Vec<DataflowEvent>) {
monitor_stream(
child_stdout,
&secret_for_stdout,
&DataflowEventKind::LeakedStdout,
true,
stdout_start,
)
});
let secret_for_stderr = Zeroizing::new(secret_bytes);
let (stderr_leaks, mut stderr_events) = monitor_stream(
child_stderr,
&secret_for_stderr,
&DataflowEventKind::LeakedStderr,
false,
start_time,
);
let (stdout_leaks, mut stdout_events) = stdout_handle.join().unwrap_or((0, Vec::new()));
let leak_count = stdout_leaks + stderr_leaks;
events.append(&mut stdout_events);
events.append(&mut stderr_events);
let status = child.wait().map_err(Error::ExecFailed)?;
let exit_code = status.code().unwrap_or(-1);
events.push(DataflowEvent {
kind: DataflowEventKind::ProcessExited,
timestamp_ms: u64::try_from(start_time.elapsed().as_millis()).unwrap_or(u64::MAX),
detail: format!("child exited with code {exit_code}"),
});
if leak_count > 0 {
log_leak_alerts(&events, leak_count, secret_name, &prepared.binary_path)?;
}
Ok(SupervisedResult {
exit_code,
leak_events: leak_count,
events,
})
}
fn build_supervised_command(prepared: &PreparedExecution, tier: SandboxTier) -> Command {
#[cfg(target_os = "linux")]
{
if matches!(tier, SandboxTier::Lockdown) {
return build_lockdown_helper_command(prepared, tier);
}
}
let mut cmd = Command::new(&prepared.exec_path);
#[cfg(target_os = "linux")]
cmd.arg0(&prepared.binary_path);
cmd.args(&prepared.args);
cmd.env_clear();
for (k, v) in &prepared.clean_env {
cmd.env(k, v);
}
for (var, val) in &prepared.env_pairs {
cmd.env(var, val.as_str());
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
#[cfg(unix)]
{
let tier_for_child = tier;
unsafe {
cmd.pre_exec(move || {
super::context::harden_child_process_inner()?;
crate::sandbox::apply_sandbox(tier_for_child)?;
Ok(())
});
}
}
#[cfg(not(unix))]
let _ = tier;
cmd
}
#[cfg(target_os = "linux")]
fn build_lockdown_helper_command(prepared: &PreparedExecution, tier: SandboxTier) -> Command {
let envseal_self =
std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("/proc/self/exe"));
let target_fd: std::os::fd::RawFd = prepared.pinned_target_fd();
let mut cmd = Command::new(&envseal_self);
cmd.arg("__sandbox_helper");
cmd.arg("--target-fd");
cmd.arg(target_fd.to_string());
cmd.arg("--arg0");
cmd.arg(&prepared.binary_path);
cmd.arg("--");
cmd.args(&prepared.args);
cmd.env_clear();
for (k, v) in &prepared.clean_env {
cmd.env(k, v);
}
for (var, val) in &prepared.env_pairs {
cmd.env(var, val.as_str());
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let tier_for_child = tier;
unsafe {
cmd.pre_exec(move || {
super::context::harden_child_process_inner()?;
crate::sandbox::apply_sandbox(tier_for_child)?;
if target_fd >= 0 {
let prev = libc::fcntl(target_fd, libc::F_GETFD);
if prev < 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fcntl(target_fd, libc::F_SETFD, prev & !libc::FD_CLOEXEC) < 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
}
cmd
}
fn monitor_stream(
stream: Option<impl Read>,
secret: &[u8],
leak_kind: &DataflowEventKind,
is_stdout: bool,
start_time: std::time::Instant,
) -> (usize, Vec<DataflowEvent>) {
let mut leaks = 0;
let mut events = Vec::new();
if let Some(mut reader) = stream {
let mut buf = [0u8; 8192];
let overlap_len = if secret.len() > 1 {
secret.len() - 1
} else {
0
};
let mut overlap = Zeroizing::new(Vec::<u8>::new());
loop {
match reader.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let chunk = &buf[..n];
let has_leak = if overlap.is_empty() {
contains_secret(chunk, secret)
} else {
let mut combined = Zeroizing::new(overlap.clone());
combined.extend_from_slice(chunk);
contains_secret(combined.as_ref(), secret)
};
if has_leak {
let redacted = redact_bytes(chunk, secret);
if is_stdout {
let _ = std::io::stdout().write_all(&redacted);
} else {
let _ = std::io::stderr().write_all(&redacted);
}
leaks += 1;
events.push(DataflowEvent {
kind: leak_kind.clone(),
timestamp_ms: u64::try_from(start_time.elapsed().as_millis())
.unwrap_or(u64::MAX),
detail: format!(
"secret detected in {} output ({n} bytes) — redacted",
if is_stdout { "stdout" } else { "stderr" }
),
});
} else if is_stdout {
let _ = std::io::stdout().write_all(chunk);
} else {
let _ = std::io::stderr().write_all(chunk);
}
if overlap_len > 0 && n >= overlap_len {
overlap = Zeroizing::new(chunk[n - overlap_len..].to_vec());
} else if overlap_len > 0 {
overlap = Zeroizing::new(chunk.to_vec());
}
}
}
}
buf.fill(0);
overlap.zeroize();
}
(leaks, events)
}
fn log_leak_alerts(
events: &[DataflowEvent],
leak_count: usize,
secret_name: &str,
binary_path: &str,
) -> Result<(), Error> {
let mut detail = format!(
"{leak_count} secret leak(s) detected and REDACTED — the secret was NOT \
exposed to the calling process"
);
for event in events {
if event.kind == DataflowEventKind::LeakedStdout
|| event.kind == DataflowEventKind::LeakedStderr
{
detail.push_str("\n └─ ");
detail.push_str(&event.detail);
}
}
let _ = crate::guard::emit_signal_inline(
crate::guard::Signal::new(
crate::guard::SignalId::scoped("execution.supervisor.leak", secret_name),
crate::guard::Category::SupervisorLeak,
crate::guard::Severity::Hostile,
"supervised child leaked secret",
detail,
"review the child binary; tighten the rule set to require Lockdown sandbox tier",
),
&crate::security_config::load_system_defaults(),
);
crate::audit::log_required(&crate::audit::AuditEvent::SupervisorLeakDetected {
secret: secret_name.to_string(),
binary: binary_path.to_string(),
leak_count,
})?;
Ok(())
}
fn contains_secret(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
fn redact_bytes(data: &[u8], secret: &[u8]) -> Zeroizing<Vec<u8>> {
if secret.is_empty() {
return Zeroizing::new(data.to_vec());
}
let marker = b"[ENVSEAL:REDACTED]";
let mut result = Zeroizing::new(Vec::with_capacity(data.len()));
let mut i = 0;
while i < data.len() {
if i + secret.len() <= data.len() && &data[i..i + secret.len()] == secret {
result.extend_from_slice(marker);
i += secret.len();
} else {
result.push(data[i]);
i += 1;
}
}
result
}
pub fn print_dataflow_report(result: &SupervisedResult, secret_name: &str) {
eprintln!();
eprintln!("╔═══════════════════════════════════════════════╗");
eprintln!("║ envseal dataflow report ║");
eprintln!("╚═══════════════════════════════════════════════╝");
eprintln!();
for event in &result.events {
let icon = match event.kind {
DataflowEventKind::SecretInjected => "🔑",
DataflowEventKind::LeakedStdout | DataflowEventKind::LeakedStderr => "🚨",
DataflowEventKind::ProcessExited => "✓",
};
eprintln!(" {icon} [{:>6}ms] {}", event.timestamp_ms, event.detail);
}
eprintln!();
if result.leak_events > 0 {
eprintln!(
" ⚠️ {} leak(s) detected and REDACTED for secret '{}'",
result.leak_events, secret_name
);
} else {
eprintln!(" ✅ no leaks detected for secret '{secret_name}'");
}
eprintln!(" exit code: {}", result.exit_code);
eprintln!();
}