use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::policy_ir::contract::SecurityContract;
use crate::sandbox::envfilter;
use crate::verify_ng::redact;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EnvironmentViolation {
ArbitraryHostVariableLeaked { key: String, value: String },
SensitiveVariableLeaked { key: String, value: String },
PathManipulation { path: String, reason: String },
InheritedEnvironmentUnscrubbed { key: String },
InternalVettoVariableLeaked { key: String },
ExplicitlyDeniedVariablePresent { key: String },
HostEnvironmentMutated { diff: Vec<String> },
PostStartMutationDetected { detail: String },
}
impl EnvironmentViolation {
pub fn key(&self) -> &str {
match self {
Self::ArbitraryHostVariableLeaked { key, .. } => key,
Self::SensitiveVariableLeaked { key, .. } => key,
Self::PathManipulation { .. } => "PATH",
Self::InheritedEnvironmentUnscrubbed { key } => key,
Self::InternalVettoVariableLeaked { key } => key,
Self::ExplicitlyDeniedVariablePresent { key } => key,
Self::HostEnvironmentMutated { .. } => "HOST_ENV",
Self::PostStartMutationDetected { .. } => "MUTATION",
}
}
pub fn reason(&self) -> String {
match self {
Self::ArbitraryHostVariableLeaked { key, .. } => {
format!("arbitrary host variable '{key}' leaked into execution without contract authorization")
}
Self::SensitiveVariableLeaked { key, .. } => {
format!("sensitive-looking variable '{key}' reached sandboxed execution")
}
Self::PathManipulation { path, reason } => {
format!("PATH manipulation detected in '{path}': {reason}")
}
Self::InheritedEnvironmentUnscrubbed { key } => {
format!("unallowed inherited variable '{key}' present in execution")
}
Self::InternalVettoVariableLeaked { key } => {
format!("internal Vetto variable '{key}' leaked to execution")
}
Self::ExplicitlyDeniedVariablePresent { key } => {
format!("variable '{key}' explicitly denied by contract was present in execution")
}
Self::HostEnvironmentMutated { diff } => {
format!("host process environment was mutated: {}", diff.join(", "))
}
Self::PostStartMutationDetected { detail } => {
format!("post-start mutation detected: {detail}")
}
}
}
}
impl std::fmt::Display for EnvironmentViolation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.reason())
}
}
#[derive(Debug, Clone)]
pub struct HarnessEnvContext {
pub isolated_home: PathBuf,
pub fixture_root: PathBuf,
pub session_nonce: String,
pub has_control_channel: bool,
pub control_downlink_val: Option<String>,
pub control_uplink_val: Option<String>,
}
#[derive(Debug, Clone)]
pub struct EnvironmentVerificationReport {
pub clean: bool,
pub allowed_by_contract: Vec<(String, String)>,
pub violations: Vec<EnvironmentViolation>,
pub host_facts: Vec<(String, String)>,
}
#[cfg(target_os = "linux")]
pub fn capture_proc_environ(pid: u32) -> Option<Vec<(String, String)>> {
let bytes = std::fs::read(format!("/proc/{pid}/environ")).ok()?;
parse_null_delimited_environ(&bytes)
}
#[cfg(not(target_os = "linux"))]
pub fn capture_proc_environ(_pid: u32) -> Option<Vec<(String, String)>> {
None
}
pub fn parse_null_delimited_environ(bytes: &[u8]) -> Option<Vec<(String, String)>> {
let mut vars = Vec::new();
for entry in bytes.split(|&b| b == 0) {
if entry.is_empty() {
continue;
}
if let Some(pos) = entry.iter().position(|&b| b == b'=') {
let key = String::from_utf8_lossy(&entry[..pos]).to_string();
let val = String::from_utf8_lossy(&entry[pos + 1..]).to_string();
vars.push((key, val));
}
}
Some(vars)
}
pub fn is_denied_by_contract(contract: &SecurityContract, key: &str) -> bool {
if contract
.environment
.redacted_patterns
.iter()
.any(|pat| envfilter::matches_wildcard_pattern(pat, key))
{
return true;
}
if let Some(prod) = &contract.production {
if prod
.installation_policy
.environment
.deny
.iter()
.any(|pat| envfilter::matches_wildcard_pattern(pat, key))
{
return true;
}
}
false
}
pub fn is_allowed_by_contract(
contract: &SecurityContract,
key: &str,
val: &str,
harness: Option<&HarnessEnvContext>,
) -> Result<(), EnvironmentViolation> {
if is_denied_by_contract(contract, key) {
return Err(EnvironmentViolation::ExplicitlyDeniedVariablePresent {
key: key.to_string(),
});
}
if let Some(expected_val) = contract.environment.explicit_vars.get(key) {
if expected_val == val {
return Ok(());
}
}
if key == "VETTO_PROD_NONCE" {
if contract.environment.inject_session_nonce && val == contract.session_nonce {
return Ok(());
} else {
return Err(EnvironmentViolation::InternalVettoVariableLeaked {
key: key.to_string(),
});
}
}
if let Some(h) = harness {
if key == "HOME" {
if Path::new(val) == h.isolated_home {
return Ok(());
} else {
return Err(EnvironmentViolation::InheritedEnvironmentUnscrubbed {
key: "HOME".to_string(),
});
}
}
if key == "USERPROFILE" && Path::new(val) == h.isolated_home {
return Ok(());
}
if (key == "VETTO_VNG_NONCE" || key == "VETTO_RUN_NONCE") && val == h.session_nonce {
return Ok(());
}
if (key == "VETTO_VNG_HOME" || key == "VETTO_RUN_HOME") && Path::new(val) == h.isolated_home
{
return Ok(());
}
if (key == "VETTO_VNG_ROOT" || key == "VETTO_FIXTURE_ROOT")
&& Path::new(val) == h.fixture_root
{
return Ok(());
}
if key == "VETTO_VNG_CONTROL_DOWNLINK" && h.has_control_channel {
if let Some(ref expected) = h.control_downlink_val {
if val != expected {
return Err(EnvironmentViolation::InternalVettoVariableLeaked {
key: key.to_string(),
});
}
}
return Ok(());
}
if key == "VETTO_VNG_CONTROL_UPLINK" && h.has_control_channel {
if let Some(ref expected) = h.control_uplink_val {
if val != expected {
return Err(EnvironmentViolation::InternalVettoVariableLeaked {
key: key.to_string(),
});
}
}
return Ok(());
}
}
if key.starts_with("VETTO_") {
return Err(EnvironmentViolation::InternalVettoVariableLeaked {
key: key.to_string(),
});
}
if envfilter::is_hard_denied(key) {
return Err(EnvironmentViolation::SensitiveVariableLeaked {
key: key.to_string(),
value: redact::redact_text(val),
});
}
if key.eq_ignore_ascii_case("PATH") {
let components: Vec<&str> = val.split(':').collect();
if components
.iter()
.any(|c| c.is_empty() || *c == "." || c.starts_with('~'))
{
return Err(EnvironmentViolation::PathManipulation {
path: val.to_string(),
reason: "contains '.', empty, or '~' relative component".to_string(),
});
}
let sanitized = envfilter::sanitize_path(val);
if val != sanitized {
return Err(EnvironmentViolation::PathManipulation {
path: val.to_string(),
reason: format!("path '{val}' differs from sanitized '{sanitized}'"),
});
}
}
let in_pass_through_vars = contract
.environment
.pass_through_vars
.iter()
.any(|pat| envfilter::matches_wildcard_pattern(pat, key));
let in_policy_pass_through = contract
.production
.as_ref()
.map(|prod| prod.installation_policy.environment.allows(OsStr::new(key)))
.unwrap_or(false);
if in_pass_through_vars || in_policy_pass_through {
return Ok(());
}
Err(EnvironmentViolation::InheritedEnvironmentUnscrubbed {
key: key.to_string(),
})
}
pub fn verify_execution_environment(
contract: &SecurityContract,
observed_env: &[(String, String)],
host_env_before: &BTreeMap<String, String>,
host_env_after: &BTreeMap<String, String>,
harness: Option<&HarnessEnvContext>,
) -> EnvironmentVerificationReport {
let mut violations = Vec::new();
let mut allowed_by_contract = Vec::new();
let mut host_facts = Vec::new();
let mut mutated_keys = Vec::new();
for (k, v) in host_env_before {
if host_env_after.get(k) != Some(v) {
mutated_keys.push(format!("mutated:{k}"));
}
}
for k in host_env_after.keys() {
if !host_env_before.contains_key(k) {
mutated_keys.push(format!("added:{k}"));
}
}
if !mutated_keys.is_empty() {
violations.push(EnvironmentViolation::HostEnvironmentMutated {
diff: mutated_keys.clone(),
});
host_facts.push((
"env-violation".to_string(),
format!("host_mutated:{}", mutated_keys.join(",")),
));
}
for (key, val) in observed_env {
match is_allowed_by_contract(contract, key, val, harness) {
Ok(()) => {
allowed_by_contract.push((key.clone(), val.clone()));
host_facts.push(("env-item".to_string(), format!("allowed_by_contract:{key}")));
}
Err(mut v) => {
if let EnvironmentViolation::InheritedEnvironmentUnscrubbed { .. } = v {
if host_env_before.contains_key(key) {
v = EnvironmentViolation::ArbitraryHostVariableLeaked {
key: key.clone(),
value: val.clone(),
};
}
}
host_facts.push(("env-item".to_string(), format!("leaked_from_host:{key}")));
host_facts.push((
"env-violation".to_string(),
format!("{}:{}", v.key(), v.reason()),
));
violations.push(v);
}
}
}
if violations.is_empty() {
host_facts.push((
"env-isolated".to_string(),
"clean:contract-conforming".to_string(),
));
host_facts.push(("vector:env-scrub".to_string(), "clean:scrubbed".to_string()));
host_facts.push((
"vector:env-hygiene".to_string(),
"clean:path-and-internal".to_string(),
));
}
EnvironmentVerificationReport {
clean: violations.is_empty(),
allowed_by_contract,
violations,
host_facts,
}
}