use anyhow::{Context as _, Result};
use openmoat_audit::{SessionWrite, Store, Written};
use openmoat_core::{Action, CompiledPolicy, Decision, Verdict};
use openmoat_hosts::Host;
use crate::approvals::{self, Grants};
use crate::home::Home;
use crate::realpath::FsPathResolver;
use crate::taint::provenance;
pub const RULE: &str = "session-written-code";
pub struct Call<'a> {
pub home: &'a Home,
pub host: Host,
pub session: &'a str,
pub action: &'a Action,
pub cwd: &'a str,
pub user_home: &'a str,
pub writes: &'a [SessionWrite],
pub within_run: bool,
}
pub fn checked(
call: &Call<'_>,
policy: &CompiledPolicy<'_>,
store: &Store,
decision: Decision,
(now_ms, sandboxed): (i64, &dyn Fn() -> bool),
) -> Result<Decision> {
if !matches!(decision.verdict, Verdict::Allow | Verdict::Defer) || call.within_run {
return Ok(decision);
}
if call.writes.is_empty() && !store.session_wrote_any(call.host.id(), call.session)? {
return Ok(decision);
}
let ci = policy.context().case_insensitive_paths;
let executed: Vec<String> = policy
.executed(call.action, &FsPathResolver)
.iter()
.map(|path| provenance::key(path, ci))
.collect();
if executed.is_empty() {
return Ok(decision);
}
let mut written = store
.session_wrote(call.host.id(), call.session, &executed)
.context("reading the files this session wrote from the audit log")?;
let now = executed
.iter()
.filter(|path| call.writes.iter().any(|w| &&w.path == path))
.map(|path| Written {
path: path.clone(),
event_id: openmoat_audit::EventId(0),
ts_ms: 0,
sha256: None,
before: None,
});
written.extend(now);
if written.is_empty() || sandboxed() {
return Ok(decision);
}
if granted_since(call, &written, now_ms)? {
let mut decision = decision;
decision.reasons.push(
"runs code this session wrote, approved with `openmoat allow` after it last wrote it"
.to_owned(),
);
return Ok(decision);
}
Ok(review(call.host, decision, &written))
}
fn granted_since(call: &Call<'_>, written: &[Written], now_ms: i64) -> Result<bool> {
let Some(reviewed) = approvals::approvable(call.action, Some(call.cwd), call.user_home) else {
return Ok(false);
};
let grants = Grants::load_in(call.home)?;
let project = approvals::project_of(
std::path::Path::new(call.cwd),
std::path::Path::new(call.user_home),
);
let scope = approvals::Scope {
host: call.host.id(),
session_id: call.session,
project: &project,
};
let Some(grant) = grants.newest(scope, &reviewed, now_ms) else {
return Ok(false);
};
Ok(written.iter().all(|w| match grant.audit_row {
Some(seen) => seen >= w.event_id.0,
None => grant.granted_at_ms > w.ts_ms,
}))
}
fn review(host: Host, decision: Decision, written: &[Written]) -> Decision {
let mut files: Vec<&str> = written.iter().map(|w| w.path.as_str()).collect();
files.sort_unstable();
files.dedup();
let mut reviewed = Decision::new(Verdict::Review);
reviewed.rules.push(RULE.to_owned());
reviewed.reasons.push(format!(
"runs code this session wrote, with no OS sandbox below the hook: {}",
files.join(", ")
));
if host.spec().reviews.allow_last_hint {
reviewed.reasons.push(
"to run it in this session, run `openmoat allow --last` in a terminal".to_owned(),
);
}
reviewed.context.extend(decision.reasons);
reviewed.context.extend(decision.context);
reviewed
}
#[cfg(test)]
mod tests;