use std::path::{Path, PathBuf};
use crate::policy::Policy;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DenyOverlapReport {
pub denied_path: PathBuf,
pub inside_grant: bool,
pub conflicting_root: Option<PathBuf>,
}
pub fn analyze_deny_overlap(policy: &Policy) -> Vec<DenyOverlapReport> {
let granted_roots: Vec<&Path> = policy
.allow_write
.iter()
.chain(policy.allow_read.iter())
.map(|root| root.as_path())
.collect();
policy
.deny_resolved
.iter()
.map(|denied| {
let conflicting = granted_roots
.iter()
.copied()
.find(|root| path_is_inside(&denied.path, root))
.map(|r| r.to_path_buf());
let inside_grant = conflicting.is_some();
DenyOverlapReport {
denied_path: denied.path.clone(),
inside_grant,
conflicting_root: conflicting,
}
})
.collect()
}
fn path_is_inside(candidate: &Path, root: &Path) -> bool {
let mut roots = root.components();
let mut candidates = candidate.components();
loop {
match (candidates.next(), roots.next()) {
(_, None) => return true,
(None, Some(_)) => return false,
(Some(cand), Some(root_component)) => {
if !component_matches(cand, root_component) {
return false;
}
}
}
}
}
fn component_matches(left: std::path::Component<'_>, right: std::path::Component<'_>) -> bool {
use std::path::Component;
match (left, right) {
(Component::Prefix(l), Component::Prefix(r)) => l
.as_os_str()
.to_string_lossy()
.eq_ignore_ascii_case(&r.as_os_str().to_string_lossy()),
(Component::RootDir, Component::RootDir) => true,
(Component::CurDir, Component::CurDir) => true,
(Component::ParentDir, Component::ParentDir) => true,
(Component::Normal(l), Component::Normal(r)) => {
#[cfg(windows)]
{
l.to_string_lossy()
.eq_ignore_ascii_case(&r.to_string_lossy())
}
#[cfg(not(windows))]
{
l == r
}
}
_ => false,
}
}
#[cfg(unix)]
use std::collections::HashMap;
#[cfg(unix)]
use std::io::Read;
#[cfg(unix)]
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
#[cfg(unix)]
use anyhow::{bail, Result};
#[cfg(unix)]
use crate::config::NetMode;
#[cfg(unix)]
use crate::sandbox;
#[cfg(unix)]
pub struct ProbeOutput {
pub stdout: String,
pub stderr: String,
}
#[cfg(unix)]
const PROBE_SCRIPT: &str = r##"for p in "$@"; do
case "$p" in
NETCHECK:*)
port=${p#NETCHECK:}
if command -v bash >/dev/null 2>&1; then
if (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
echo "NET|reachable"
else
echo "NET|unreachable"
fi
else
echo "NET|nobash"
fi
;;
WRITECHECK:*)
target=${p#WRITECHECK:}
if dd if=/dev/null of="$target" bs=1 count=1 2>/dev/null; then
echo "WRITE|allowed"
else
echo "WRITE|denied"
fi
;;
*)
if [ -d "$p" ]; then
leak=0
for f in "$p"/* "$p"/.[!.]* "$p"/..?*; do
[ -f "$f" ] || continue
if dd if="$f" of=/dev/null bs=1 count=1 >/dev/null 2>&1; then leak=1; break; fi
done
if [ "$leak" -eq 0 ]; then
echo "D|$p|contents-denied"
else
echo "D|$p|content-readable"
fi
else
n=$(wc -c <"$p" 2>/dev/null) || { echo "F|$p|unreadable"; continue; }
echo "F|$p|$n"
fi
;;
esac
done"##;
#[cfg(unix)]
pub fn run_probe_script(
pol: &Policy,
project: &Path,
script_args: Vec<String>,
) -> Result<ProbeOutput> {
let backend = sandbox::Backend::detect(NetMode::Off, false)?;
let mut agent_cmd = vec![
"/bin/sh".to_string(),
"-c".to_string(),
PROBE_SCRIPT.to_string(),
"vetto-probe".to_string(),
];
agent_cmd.extend(script_args);
let (out_r, out_w) = pipe2()?;
let (err_r, err_w) = pipe2()?;
let stdio = sandbox::StdioMode::Captured {
stdout_w: out_w.as_raw_fd(),
stderr_w: err_w.as_raw_fd(),
};
let unprepared = crate::sandbox::production::UnpreparedProductionExecution::new(
backend,
pol.clone(),
agent_cmd,
project.to_path_buf(),
HashMap::new(),
NetMode::Off,
None,
stdio,
"probe".to_string(),
);
let prepared = unprepared.prepare()?;
let spawned = prepared.spawn()?;
drop(out_w);
drop(err_w);
let mut output = String::new();
let mut out_file: std::fs::File = out_r.into();
let _ = out_file.read_to_string(&mut output);
let mut eout = String::new();
let mut err_file: std::fs::File = err_r.into();
let _ = err_file.read_to_string(&mut eout);
let _prod_res = spawned.wait_collect();
Ok(ProbeOutput {
stdout: output,
stderr: eout,
})
}
#[cfg(unix)]
fn pipe2() -> Result<(OwnedFd, OwnedFd)> {
let mut fds = [0 as libc::c_int; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
bail!("pipe: {}", std::io::Error::last_os_error());
}
for fd in fds {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
let error = std::io::Error::last_os_error();
unsafe {
libc::close(fds[0]);
libc::close(fds[1]);
}
bail!("fcntl(F_GETFD): {error}");
}
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 {
let error = std::io::Error::last_os_error();
unsafe {
libc::close(fds[0]);
libc::close(fds[1]);
}
bail!("fcntl(F_SETFD, FD_CLOEXEC): {error}");
}
}
Ok((unsafe { OwnedFd::from_raw_fd(fds[0]) }, unsafe {
OwnedFd::from_raw_fd(fds[1])
}))
}