use std::io::Write as _;
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Duration;
use anyhow::{Context as _, Result, bail};
use openmoat_audit::{NewEvent, Store};
use openmoat_core::{Decision, Verdict};
use openmoat_hosts::Host;
use openmoat_proxy::{LocalService, constant_time_eq};
use zeroize::Zeroizing;
use super::wire::{MAX_OUTPUT, REQUEST_MAGIC, Reply, Request};
use super::{SOCKET_ENV, Stream, TOKEN_ENV};
use crate::home::Home;
use crate::install::GUARD_BUDGET_S;
use crate::taint::run::{LINK_HOST, RUN_SESSION_ENV};
pub const RULE_TOKEN: &str = "run-guard-token";
pub struct Service {
home: Home,
run: String,
token: Zeroizing<Vec<u8>>,
moat: PathBuf,
}
impl Service {
pub fn new(home: Home, run: &str, token: &[u8], moat: PathBuf) -> Self {
Self {
home,
run: run.to_owned(),
token: Zeroizing::new(token.to_vec()),
moat,
}
}
pub fn answer(&self, mut stream: impl Stream) -> Result<()> {
stream.set_timeouts(Duration::from_secs(GUARD_BUDGET_S))?;
let request = Request::read_from(&mut stream)?;
if !constant_time_eq(&request.token, &self.token) {
self.record_refused(&request.host)?;
bail!("a hook call with a wrong token");
}
let host: Host = request.host.parse()?;
let reply = self.decide(host, &request)?;
reply.write_to(&mut stream)
}
fn decide(&self, host: Host, request: &Request) -> Result<Reply> {
let mut command = Command::new(&self.moat);
command
.args(["guard", "--host", host.id()])
.env(RUN_SESSION_ENV, &self.run)
.env_remove(SOCKET_ENV)
.env_remove(TOKEN_ENV)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if request.continue_cli {
command.env(openmoat_hosts::CONTINUE_ENV, "1");
} else {
command.env_remove(openmoat_hosts::CONTINUE_ENV);
}
let cwd = Path::new(&request.cwd);
if cwd.is_absolute() && cwd.is_dir() {
command.current_dir(cwd);
}
let mut child = command
.spawn()
.with_context(|| format!("starting {}", self.moat.display()))?;
let mut stdin = child
.stdin
.take()
.context("no standard input for the guard")?;
let payload = request.payload.clone();
let writer = std::thread::spawn(move || stdin.write_all(payload.as_bytes()));
let output = child.wait_with_output()?;
let _ = writer.join();
let mut reply = Reply {
nonce: request.nonce.clone(),
code: if output.status.code() == Some(0) {
0
} else {
2
},
stdout: output.stdout,
stderr: output.stderr,
};
if reply.stdout.len() > MAX_OUTPUT || reply.stderr.len() > MAX_OUTPUT {
reply.code = 2;
reply.stdout.clear();
reply.stderr =
b"moat: the guard's answer is too large to relay, so the call is denied\n".to_vec();
}
Ok(reply)
}
fn record_refused(&self, claimed: &str) -> Result<()> {
let host = claimed
.parse::<Host>()
.map_or("an unknown host", |h| h.id());
let decision = Decision::single(
Verdict::Deny,
RULE_TOKEN,
format!(
"a hook call for {host} came with a wrong token for this `openmoat run`, so it was \
not decided"
),
);
let store = Store::open_existing(&self.home.audit_path())?;
store.record(&NewEvent {
host: LINK_HOST,
session_id: &self.run,
call_id: None,
cwd: None,
tool: "guard",
action: None,
decision: &decision,
latency_us: 0,
})?;
Ok(())
}
}
impl LocalService for Service {
fn prefix(&self) -> &[u8] {
REQUEST_MAGIC
}
fn serve(&self, client: &TcpStream) {
let _ = self.answer(client);
}
}