use std::path::Path;
use std::time::Duration;
use kovra_core::{
AuditAction, AuditEvent, AuditSink, Clock, ConfirmOutcome, ConfirmRequest, Confirmer, EnvRefs,
EnvSource, Keyring, Origin, PROD, Registry, SecretProvider, Sensitivity,
inject_requires_allowlist, inject_requires_confirmation, outcome_result, resolve,
};
use crate::allowlist::Allowlist;
use crate::error::WrapperError;
use crate::runner::{Command, Output, ProcessRunner};
use crate::sanitize::mask_secrets;
pub struct Wrapper<'a> {
pub registry: &'a Registry,
pub keyring: &'a dyn Keyring,
pub env_source: &'a dyn EnvSource,
pub provider: &'a dyn SecretProvider,
pub confirmer: &'a dyn Confirmer,
pub audit: &'a dyn AuditSink,
pub clock: &'a dyn Clock,
pub allowlist: &'a Allowlist,
pub runner: &'a dyn ProcessRunner,
pub confirm_timeout: Duration,
pub sanitize_output: bool,
pub requesting_process: Option<String>,
}
impl Wrapper<'_> {
pub fn run(
&self,
refs: &EnvRefs,
env: &str,
project_override: Option<&str>,
program: &Path,
args: &[String],
origin: Origin,
) -> Result<Output, WrapperError> {
let resolved = resolve(
refs,
env,
self.registry,
self.keyring,
self.env_source,
self.provider,
self.audit,
self.clock,
origin,
project_override,
)?;
let mut allowlist_gated: Vec<GatedVar> = Vec::new();
let mut confirm_gated: Vec<GatedVar> = Vec::new();
for v in &resolved.vars {
let Some(coordinate) = v.coordinate.clone() else {
continue;
};
let sensitivity = v.sensitivity.unwrap_or(Sensitivity::Low);
let is_prod = v.environment == PROD;
if inject_requires_allowlist(sensitivity, is_prod) {
allowlist_gated.push(GatedVar {
coordinate: coordinate.clone(),
environment: v.environment.clone(),
sensitivity,
});
}
if inject_requires_confirmation(sensitivity) {
confirm_gated.push(GatedVar {
coordinate,
environment: v.environment.clone(),
sensitivity,
});
}
}
let resolved_command = render_argv(program, args);
if !allowlist_gated.is_empty() && !self.allowlist.allows(program) {
for g in &allowlist_gated {
self.record(
AuditEvent::new(self.clock, AuditAction::Deny, "denied:not-allowlisted")
.at(&g.coordinate, &g.environment)
.by(origin),
);
}
return Err(WrapperError::NotAllowlisted {
program: program.display().to_string(),
});
}
if !confirm_gated.is_empty() {
let coordinates = confirm_gated
.iter()
.map(|g| g.coordinate.as_str())
.collect::<Vec<_>>()
.join(", ");
let mut req = ConfirmRequest::new(
coordinates,
representative_sensitivity(&confirm_gated),
representative_environment(&confirm_gated),
origin,
)
.with_command(resolved_command);
if let Some(proc) = self.requesting_process.as_deref() {
req = req.with_requesting_process(proc);
}
let outcome = self.confirmer.confirm(&req, self.confirm_timeout);
let action = match outcome {
ConfirmOutcome::Approved => AuditAction::Approve,
ConfirmOutcome::Denied => AuditAction::Deny,
ConfirmOutcome::TimedOut => AuditAction::Timeout,
};
for g in &confirm_gated {
self.record(
AuditEvent::new(self.clock, action, outcome_result(outcome))
.at(&g.coordinate, &g.environment)
.by(origin),
);
}
match outcome {
ConfirmOutcome::Approved => {}
ConfirmOutcome::Denied => return Err(WrapperError::ConfirmationDenied),
ConfirmOutcome::TimedOut => return Err(WrapperError::ConfirmationTimedOut),
}
}
let mut env = Vec::with_capacity(resolved.vars.len());
let mut secret_names: Vec<String> = Vec::new();
for v in resolved.vars {
if let Some(coordinate) = &v.coordinate {
self.record(
AuditEvent::new(self.clock, AuditAction::Inject, "injected")
.at(coordinate, &v.environment)
.by(origin),
);
secret_names.push(v.name.clone());
}
env.push((v.name, v.value));
}
let command = Command {
program: program.to_path_buf(),
args: args.to_vec(),
env,
};
let mut output = self.runner.run(&command)?;
if self.sanitize_output {
let secrets: Vec<&[u8]> = command
.env
.iter()
.filter(|(name, _)| secret_names.contains(name))
.map(|(_, v)| v.expose())
.collect();
output.stdout = mask_secrets(&output.stdout, &secrets);
output.stderr = mask_secrets(&output.stderr, &secrets);
}
Ok(output)
}
fn record(&self, event: AuditEvent) {
let _ = self.audit.record(&event);
}
}
struct GatedVar {
coordinate: String,
environment: String,
sensitivity: Sensitivity,
}
fn render_argv(program: &Path, args: &[String]) -> String {
let mut s = program.display().to_string();
for a in args {
s.push(' ');
s.push_str(a);
}
s
}
fn representative_sensitivity(gated: &[GatedVar]) -> Sensitivity {
if gated.iter().any(|g| g.sensitivity == Sensitivity::High) {
Sensitivity::High
} else {
gated[0].sensitivity
}
}
fn representative_environment(gated: &[GatedVar]) -> String {
if let Some(g) = gated.iter().find(|g| g.environment == PROD) {
g.environment.clone()
} else {
gated[0].environment.clone()
}
}