envseal 0.3.12

Write-only secret vault with process-level access control — post-agent secret management
Documentation
//! Environment-variable hostility assessment and child-process sanitization.
//!
//! `LD_PRELOAD` / `DYLD_INSERT_LIBRARIES` etc. inject attacker-controlled
//! code into every child process, so we treat them as a hard block
//! (configurable). Less-fatal vars like `LD_LIBRARY_PATH`, `PYTHONPATH`,
//! `NODE_OPTIONS` are stripped from the child env and optionally block
//! the parent operation when the active security tier requires it.

use std::collections::HashMap;

use crate::error::Error;

/// Environment variables that provide direct code injection into child processes.
///
/// If any of these are set, a malicious library WILL be loaded into the child
/// process. This is a hard block — no override.
pub(crate) const INJECT_ENV_VARS: &[&str] = &[
    // Linux: ld.so loads these before main()
    "LD_PRELOAD",
    "LD_PRELOAD_32",
    "LD_PRELOAD_64",
    "LD_AUDIT",
    // macOS: dyld loads these before main()
    "DYLD_INSERT_LIBRARIES",
];

/// Environment variables that modify library resolution but don't inject code directly.
///
/// These are commonly set on developer workstations (CUDA, custom builds).
/// Since the inject pipeline calls `env_clear()` on the child, these only
/// affect envseal itself (which is statically linked). Warn but don't block.
pub(crate) const SUSPICIOUS_ENV_VARS: &[&str] = &[
    "LD_LIBRARY_PATH",
    "LD_DEBUG",
    "LD_DEBUG_OUTPUT",
    "LD_DYNAMIC_WEAK",
    "LD_ORIGIN_PATH",
    "LD_PROFILE",
    "LD_SHOW_AUXV",
    "DYLD_LIBRARY_PATH",
    "DYLD_FRAMEWORK_PATH",
    "DYLD_FALLBACK_LIBRARY_PATH",
    "PYTHONPATH",
    "BASH_ENV",
    "ENV",
    "NODE_OPTIONS",
    "RUBYOPT",
    "PERL5LIB",
    "JAVA_TOOL_OPTIONS",
    "GLIBC_TUNABLES",
    "GCONV_PATH",
    "LOCPATH",
    "PERL5OPT",
    "JDK_JAVA_OPTIONS",
    "_JAVA_OPTIONS",
];

/// Threat level assessed from process environment variables.
#[derive(Debug, PartialEq)]
pub enum EnvironmentThreatLevel {
    /// No hostile or suspicious variables detected.
    Safe,
    /// Suspicious variables like `LD_LIBRARY_PATH` detected.
    Degraded(String),
    /// Direct injection variables like `LD_PRELOAD` detected.
    Hostile(String),
}

/// Assess the process environment for injection and manipulation variables.
#[must_use]
pub fn assess_environment() -> EnvironmentThreatLevel {
    let mut hostile = Vec::new();
    for var in INJECT_ENV_VARS {
        if std::env::var_os(var).is_some() {
            hostile.push(*var);
        }
    }

    if !hostile.is_empty() {
        return EnvironmentThreatLevel::Hostile(format!(
            "direct injection variables detected: {}",
            hostile.join(", ")
        ));
    }

    let mut degraded = Vec::new();
    for var in SUSPICIOUS_ENV_VARS {
        if std::env::var_os(var).is_some() {
            degraded.push(*var);
        }
    }

    if !degraded.is_empty() {
        return EnvironmentThreatLevel::Degraded(format!(
            "suspicious environment variables set: {}",
            degraded.join(", ")
        ));
    }

    EnvironmentThreatLevel::Safe
}

/// Run the environment-injection assessment and emit signals.
///
/// Each detected hostile / suspicious env var becomes one
/// [`super::Signal`] under [`super::Category::EnvironmentInjection`].
/// `INJECT_ENV_VARS` matches produce `Hostile`-severity signals
/// (default policy: block under Hardened+); `SUSPICIOUS_ENV_VARS`
/// matches produce `Degraded` (default: warn-with-friction under
/// Standard, block under Hardened+).
#[must_use]
pub fn assess_env_signals(_ctx: &super::DetectorContext) -> Vec<super::Signal> {
    let mut signals = Vec::new();

    for var in INJECT_ENV_VARS {
        if std::env::var_os(var).is_some() {
            signals.push(super::Signal::new(
                super::SignalId::scoped("env.preload", var),
                super::Category::EnvironmentInjection,
                super::Severity::Hostile,
                "dynamic-loader injection variable set",
                format!("`{var}` is set in our parent environment — every child we exec will load attacker-controlled code"),
                "unset the variable before invoking envseal, or run from a clean shell",
            ));
        }
    }

    for var in SUSPICIOUS_ENV_VARS {
        if std::env::var_os(var).is_some() {
            signals.push(super::Signal::new(
                super::SignalId::scoped("env.suspicious_loader_var", var),
                super::Category::EnvironmentInjection,
                super::Severity::Degraded,
                "suspicious loader configuration variable set",
                format!("`{var}` modifies library resolution"),
                "review whether this var is intentional in this shell",
            ));
        }
    }

    signals
}

/// Enforce environment-injection policy at an execution boundary.
///
/// Single source of truth: every detected env-injection signal is
/// run through the user's [`crate::guard::Policy`] (default table
/// plus any `signal_overrides` / `tier_overrides` from
/// `security.toml`). The first signal whose decided
/// [`crate::guard::Action`] is `Block` aborts execution. Lower-severity
/// outcomes (`Warn` / `Log`) are surfaced on stderr — there is no
/// challenge gate at this stage because env-injection runs before
/// any GUI prompt.
///
/// # Behavior under default policy
///
/// | Tier      | `LD_PRELOAD` set      | `LD_LIBRARY_PATH` set |
/// |-----------|-----------------------|-----------------------|
/// | Standard  | Warn (proceed)        | Warn (proceed)        |
/// | Hardened  | **Block**             | **Block**             |
/// | Lockdown  | **Block**             | **Block**             |
///
/// Override per-signal in `security.toml`:
/// ```toml
/// [signal_overrides]
/// "env.suspicious_loader_var.ld_library_path" = "ignore"
/// ```
///
/// # Errors
/// [`Error::EnvironmentCompromised`] for the first signal whose
/// decided action is `Block`. The error message embeds the signal's
/// label, detail, and mitigation hint so the user knows exactly
/// which variable triggered and how to clear it.
pub fn enforce_env_policy(config: &crate::security_config::SecurityConfig) -> Result<(), Error> {
    // Ambient context — env-var scanning doesn't read any per-op fields.
    let ctx = super::DetectorContext::ambient();
    let signals = assess_env_signals(&ctx);
    // The parallel decide-and-eprintln ladder this function used to
    // implement is exactly what `emit_signals_inline` consolidates
    // for every other in-flight Signal source — single rendering
    // function, single audit-log path, single block conversion.
    super::emit_signals_inline(signals, config)
}

/// Build a sanitized environment for the child process.
///
/// Copies the current environment but strips all injection and suspicious
/// variables. Returns a map of variable name → value for the clean environment.
#[must_use]
pub fn sanitized_env() -> HashMap<String, String> {
    let blocked: std::collections::HashSet<&str> = INJECT_ENV_VARS
        .iter()
        .chain(SUSPICIOUS_ENV_VARS.iter())
        .copied()
        .collect();

    std::env::vars()
        .filter(|(key, _)| !blocked.contains(key.as_str()))
        .collect()
}