use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Result, bail};
use openmoat_core::{Policy, RuleGroup};
use crate::context::{absolute, path_string};
use crate::home::Home;
use crate::integrity::Lock;
const KERNEL_SELF: &str = "kernel-self";
pub fn recorded_binary(home: &Home) -> Result<PathBuf> {
match Lock::load(&home.lock_path()) {
Ok(lock) => Ok(PathBuf::from(lock.binary)),
Err(_) => super::hook_binary(),
}
}
pub fn sandbox_paths(binary: &Path) -> Vec<String> {
let recorded = absolute(binary).unwrap_or_else(|_| binary.to_path_buf());
let mut paths = vec![recorded.clone()];
if let (Some(dir), Some(name)) = (recorded.parent(), recorded.file_name()) {
paths.push(dir.to_path_buf());
if let Ok(real) = fs::canonicalize(dir) {
paths.push(real.join(name));
paths.push(real);
}
}
if let Some(other) =
super::other_name(&recorded).filter(|other| fs::symlink_metadata(other).is_ok())
{
paths.extend(other.file_name().and_then(|name| {
let real = fs::canonicalize(other.parent()?).ok()?;
Some(real.join(name))
}));
paths.push(other);
}
literal(&paths)
}
pub fn protected_paths(binary: &Path) -> Vec<String> {
let mut paths: Vec<PathBuf> = sandbox_paths(binary).iter().map(PathBuf::from).collect();
let other = super::other_name(&absolute(binary).unwrap_or_else(|_| binary.to_path_buf()));
paths.extend(other.clone());
for path in std::iter::once(binary).chain(other.as_deref()) {
if let Ok(target) = fs::canonicalize(path) {
paths.extend(target.parent().map(Path::to_path_buf));
paths.push(target);
}
}
literal(&paths)
}
pub fn protect_recorded(home: &Home, policy: &mut Policy) -> Result<()> {
protect(policy, &protected_paths(&recorded_binary(home)?));
Ok(())
}
fn literal(paths: &[PathBuf]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for path in paths.iter().map(|p| path_string(p)) {
if !has_glob_syntax(&path) && !out.contains(&path) {
out.push(path);
}
}
out
}
pub fn protect(policy: &mut Policy, paths: &[String]) {
let index = if let Some(index) = policy.deny.iter().position(|g| g.id == KERNEL_SELF) {
index
} else {
policy.deny.push(RuleGroup {
id: KERNEL_SELF.to_owned(),
reason: Some("the openmoat binary every installed hook runs".to_owned()),
..RuleGroup::default()
});
policy.deny.len() - 1
};
let writes = &mut policy.deny[index].fs_write;
for path in paths {
if !writes.contains(path) {
writes.push(path.clone());
}
}
}
pub fn refuse_writable(binary: &Path, writable: &[String]) -> Result<()> {
let target = fs::canonicalize(binary).ok();
for path in std::iter::once(binary).chain(target.as_deref()) {
if has_glob_syntax(&path_string(path)) {
bail!(
"refusing to set up hooks for {}: the path has glob characters (`*?[]{{}}`), \
so no sandbox can protect it by name; install openmoat where the path has none",
path.display()
);
}
}
let mut places: Vec<(PathBuf, &str)> = Vec::new();
let cwd = std::env::current_dir()?;
if let Some(project) = crate::project::root_of(&cwd, &crate::home::user_home()?) {
places.push((project, "inside the project of the current directory"));
}
for dir in temp_dirs() {
places.push((dir, "in a temp directory"));
}
let sandboxed = "in a directory the agents' sandboxes may write";
places.extend(writable.iter().map(|dir| (PathBuf::from(dir), sandboxed)));
for (place, why) in &places {
let real = fs::canonicalize(place).ok();
let under = |path: &Path| {
std::iter::once(place)
.chain(&real)
.any(|dir| crate::project::contains(dir, path))
};
if std::iter::once(binary).chain(target.as_deref()).any(under) {
bail!(
"refusing to set up hooks for {}: it is {why} ({}), where an agent could \
replace the binary every hook runs; install openmoat outside it (for example \
with `cargo install`, Homebrew or the release installer) and run that openmoat's \
`init`",
binary.display(),
place.display()
);
}
}
Ok(())
}
fn temp_dirs() -> Vec<PathBuf> {
let mut dirs = vec![std::env::temp_dir()];
if cfg!(unix) {
dirs.push(PathBuf::from("/tmp"));
}
dirs
}
fn has_glob_syntax(path: &str) -> bool {
path.contains(['*', '?', '[', ']', '{', '}'])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calls_see_the_link_target_and_sandboxes_what_an_upgrade_keeps() {
let dir = tempfile::tempdir().unwrap();
let real = fs::canonicalize(dir.path()).unwrap();
let target = real.join("release/moat-dev");
fs::create_dir_all(target.parent().unwrap()).unwrap();
fs::write(&target, b"").unwrap();
let release = path_string(&real.join("release"));
assert_eq!(
sandbox_paths(&target),
[path_string(&target), release.clone()]
);
assert_eq!(protected_paths(&target), sandbox_paths(&target));
#[cfg(unix)]
{
let link = real.join("bin/kernel");
fs::create_dir_all(link.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
let bin = path_string(&real.join("bin"));
assert_eq!(sandbox_paths(&link), [path_string(&link), bin.clone()]);
assert_eq!(
protected_paths(&link),
[path_string(&link), bin, release, path_string(&target)]
);
}
assert_eq!(
protected_paths(&real.join("a[1]/moat")),
Vec::<String>::new()
);
let refused = refuse_writable(&real.join("a*/moat"), &[]).unwrap_err();
assert!(refused.to_string().contains("glob characters"), "{refused}");
}
#[test]
fn the_other_name_is_protected_and_sandboxed_only_when_present() {
let dir = tempfile::tempdir().unwrap();
let tools = fs::canonicalize(dir.path()).unwrap().join("tools");
fs::create_dir_all(&tools).unwrap();
let (openmoat, moat) = (tools.join("openmoat"), tools.join("moat"));
fs::write(&openmoat, b"").unwrap();
let alias = path_string(&moat);
assert!(
!sandbox_paths(&openmoat).contains(&alias),
"nothing to mount"
);
assert!(
protected_paths(&openmoat).contains(&alias),
"none can be planted"
);
fs::write(&moat, b"").unwrap();
assert!(sandbox_paths(&openmoat).contains(&alias));
assert!(protected_paths(&moat).contains(&path_string(&openmoat)));
assert!(sandbox_paths(&moat).contains(&path_string(&openmoat)));
}
#[test]
fn protect_joins_kernel_self_once_or_creates_it() {
let paths = vec!["/opt/t/moatd".to_owned(), "/opt/t".to_owned()];
let mut policy = Policy::parse(openmoat_core::DEFAULT_POLICY).unwrap();
let before = policy.deny.len();
protect(&mut policy, &paths);
protect(&mut policy, &paths);
assert_eq!(policy.deny.len(), before);
let group = policy.deny.iter().find(|g| g.id == KERNEL_SELF).unwrap();
let count = |p: &str| group.fs_write.iter().filter(|w| *w == p).count();
assert_eq!((count("/opt/t/moatd"), count("/opt/t")), (1, 1));
let mut bare = Policy::parse("version: 1\n").unwrap();
protect(&mut bare, &paths);
assert_eq!(bare.deny[0].id, KERNEL_SELF);
assert_eq!(bare.deny[0].fs_write, paths);
bare.lint().unwrap();
}
}