#![cfg(not(target_arch = "wasm32"))]
use crate::command::{Command, Response};
use exfiltrate_internal::args::{ArgKind, ArgSpec, ParsedArgs};
pub(crate) struct Env;
static ARGS: &[ArgSpec] = &[
ArgSpec::flag(
"filter",
"only show variables whose name contains this substring",
ArgKind::String,
),
ArgSpec::flag(
"show-secrets",
"print the values of variables that look secret",
ArgKind::Bool,
),
ArgSpec::flag(
"argv",
"show the command line instead of the environment",
ArgKind::Bool,
),
];
const REDACTED: &str = "<redacted>";
impl Command for Env {
fn name(&self) -> &'static str {
"env"
}
fn short_description(&self) -> &'static str {
"Shows the process environment and command line, with secret-looking values redacted."
}
fn full_description(&self) -> &'static str {
"Shows the environment variables and command line of the process being debugged.
This command is NOT registered by default. Enable it with
`Config::default().with_batteries(Batteries::all())`, or by setting `batteries.env`.
Values whose variable name matches a redaction pattern — `secret`, `token`, `key`,
`password` and friends, configurable via `Config::env_redact_patterns` — are printed
as `<redacted>`. `--show-secrets` prints them in full, and does so over whatever
transport you are using, so decide deliberately.
Redaction is a heuristic. A variable named `DEPLOY_URL` holding a signed URL is not
matched by any name pattern and will be printed. If a value must not leave the
process, do not enable this command."
}
fn args(&self) -> &'static [ArgSpec] {
ARGS
}
fn execute(&self, args: Vec<String>) -> Result<Response, Response> {
let parsed = ParsedArgs::parse(self.args(), args).map_err(Response::String)?;
let config = crate::config_snapshot();
if parsed.boolean("argv") {
let mut out = String::from("argv:\n");
for (index, arg) in std::env::args().enumerate() {
out.push_str(&format!(" [{index}] {arg}\n"));
}
return Ok(out.into());
}
let mut vars: Vec<(String, String)> = std::env::vars().collect();
vars.sort();
Ok(render(
&vars,
parsed.get("filter"),
parsed.boolean("show-secrets"),
|name| config.should_redact(name),
)
.into())
}
}
fn render(
vars: &[(String, String)],
filter: Option<&str>,
show_secrets: bool,
should_redact: impl Fn(&str) -> bool,
) -> String {
let shown: Vec<&(String, String)> = vars
.iter()
.filter(|(name, _)| filter.is_none_or(|needle| name.contains(needle)))
.collect();
if shown.is_empty() {
return match filter {
Some(needle) => format!("no environment variable name contains {needle:?}\n"),
None => "the environment is empty\n".to_string(),
};
}
let mut redacted_count = 0usize;
let mut out = String::new();
for (name, value) in &shown {
if should_redact(name) && !show_secrets {
redacted_count += 1;
out.push_str(&format!("{name}={REDACTED}\n"));
} else {
out.push_str(&format!("{name}={value}\n"));
}
}
if redacted_count > 0 {
out.push_str(&format!(
"\n{redacted_count} value(s) redacted by name. Use --show-secrets to print them.\n"
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Vec<(String, String)> {
[
("HOME", "/home/demo"),
("AWS_SECRET_ACCESS_KEY", "hunter2"),
("PATH", "/usr/bin"),
]
.into_iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.collect()
}
fn looks_secret(name: &str) -> bool {
crate::Config::default().should_redact(name)
}
#[test]
fn secret_looking_values_are_hidden_and_the_count_is_reported() {
let text = render(&sample(), None, false, looks_secret);
assert!(text.contains("HOME=/home/demo"), "{text}");
assert!(
text.contains(&format!("AWS_SECRET_ACCESS_KEY={REDACTED}")),
"{text}"
);
assert!(!text.contains("hunter2"), "{text}");
assert!(text.contains("1 value(s) redacted"), "{text}");
}
#[test]
fn show_secrets_prints_them_and_stops_claiming_anything_was_redacted() {
let text = render(&sample(), None, true, looks_secret);
assert!(text.contains("AWS_SECRET_ACCESS_KEY=hunter2"), "{text}");
assert!(!text.contains("redacted"), "{text}");
}
#[test]
fn the_filter_narrows_by_name() {
let text = render(&sample(), Some("PATH"), false, looks_secret);
assert!(text.contains("PATH=/usr/bin"), "{text}");
assert!(!text.contains("HOME"), "{text}");
}
#[test]
fn an_empty_result_says_why_rather_than_printing_nothing() {
let text = render(&sample(), Some("NOPE"), false, looks_secret);
assert!(
text.contains("no environment variable name contains \"NOPE\""),
"{text}"
);
}
#[test]
fn the_command_reads_the_real_environment_and_still_redacts() {
let text = Env.execute(Vec::new()).unwrap().into_string();
assert!(!text.is_empty());
let argv = Env
.execute(vec!["--argv".to_string()])
.unwrap()
.into_string();
assert!(argv.starts_with("argv:"), "{argv}");
}
}