use std::io::{self, BufRead as _, Write as _};
use anyhow::{Context as _, Result};
use openmoat_audit::Event;
use openmoat_core::{Action, Verdict};
use openmoat_hosts::Host;
use crate::approvals::{Grants, Overlay};
use crate::cli::{AllowArgs, DoctorArgs};
use crate::exit::Code;
use crate::home::Home;
use crate::install::{HookState, HostConfig};
use crate::integrity::Lock;
const MS_PER_DAY: i64 = 86_400_000;
pub fn run() -> Result<Code> {
let home = Home::locate()?;
let lock_path = home.lock_path();
if !lock_path.is_file() {
println!("OpenMoat is not set up here; run `moat init`.");
return Ok(Code::Usage);
}
let person = crate::terminal::interactive();
let now = crate::time::now_ms();
let today = now - now.rem_euclid(MS_PER_DAY);
let store = home.open_audit()?;
println!("{}", health(&store.since(today, None)?)?);
let drift = Lock::load(&lock_path)?.verify();
if !drift.is_empty() {
for d in &drift {
println!("✗ lock {d}");
}
if !person {
println!("A person must accept these at a terminal: `moat doctor --accept`.");
return Ok(Code::Usage);
}
if !matches!(
answer("Accept these changes? [y/N] ")?.as_str(),
"y" | "yes"
) {
println!("Nothing changed; every call is denied until a person accepts.");
println!("Run `moat doctor` to look at the changes.");
return Ok(Code::Usage);
}
let code = super::doctor::run(&DoctorArgs {
accept: true,
verbose: false,
})?;
if !Lock::load(&lock_path)?.verify().is_empty() {
return Ok(code);
}
}
let Some((event, host, command)) = pending_ask(&home, &store, today, now)? else {
println!("Nothing needs you.");
println!("`moat show` lists recent decisions; `moat --help` lists every command.");
return Ok(Code::Ok);
};
println!(
"{} asked to run \"{command}\" (rule {}, session {})",
host.display_name(),
event.rules.join(", "),
event.session_id
);
if !person {
println!("A person can allow it at a terminal: `moat allow --last` for this session,");
println!("`moat allow --last --always` for good.");
return Ok(Code::Ok);
}
let always = match answer("Allow? [o]nce for this session / [a]lways / [n]o ")?.as_str() {
"o" | "once" => false,
"a" | "always" => true,
_ => {
println!("Nothing changed.");
return Ok(Code::Ok);
}
};
super::allow::run(&AllowArgs {
command: Some(command),
last: false,
host: (!always).then_some(host),
session: (!always).then_some(event.session_id),
always,
site: None,
dir: None,
remove: None,
})
}
fn health(today: &[Event]) -> Result<String> {
let binary = crate::install::hook_binary()?;
let mut protected = Vec::new();
for host in Host::ALL {
if matches!(
HostConfig::for_host(host)?.state(&binary),
HookState::Installed
) {
protected.push(host.display_name());
}
}
let agents = if protected.is_empty() {
"No agent is protected (run `moat init`)".to_owned()
} else {
format!("Protecting {}", protected.join(", "))
};
let count = |v: Verdict| today.iter().filter(|e| e.verdict == v).count();
Ok(format!(
"{agents} · today: {} decisions, {} denied, {} asked",
today.len(),
count(Verdict::Deny),
count(Verdict::Ask)
))
}
fn pending_ask(
home: &Home,
store: &openmoat_audit::Store,
today: i64,
now: i64,
) -> Result<Option<(Event, Host, String)>> {
let Some(event) = super::allow::newest_shell_ask(store)?.filter(|e| e.ts_ms >= today) else {
return Ok(None);
};
let Some(Action::Shell { command }) = event.action.clone() else {
return Ok(None);
};
let host: Host = event.host.parse()?;
let granted =
Grants::load(&home.grants_path())?.matches(&event.host, &event.session_id, &command, now);
let pattern = openmoat_core::literal_shell_pattern(&command).ok();
let permanent = Overlay::load(&home.overlay_path())?
.allow
.iter()
.any(|g| pattern.as_ref().is_some_and(|p| g.shell.contains(p)));
Ok((!granted && !permanent).then_some((event, host, command)))
}
fn answer(question: &str) -> Result<String> {
print!("{question}");
io::stdout().flush()?;
let mut line = String::new();
io::stdin()
.lock()
.read_line(&mut line)
.context("reading the answer")?;
Ok(line.trim().to_ascii_lowercase())
}