use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::host_paths::{self, DeclaredHostPath, HostPath, HostPathAccess, HostPathContext};
use crate::process_driver::{Description, HarnessNeeds, PathAccess};
use crate::types::CapabilityPolicy;
pub struct HarnessGrants {
pub report: HarnessReport,
pub granted_paths: Vec<HostPath>,
pub refusal: Option<crate::errors::RuntimeError>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HarnessReport {
pub driver: String,
pub binary: Option<HarnessBinaryReport>,
pub paths: Vec<HarnessPathReport>,
pub hosts: Vec<HarnessHostReport>,
pub env: Vec<HarnessEnvReport>,
pub unresolved: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HarnessBinaryReport {
pub path: String,
pub interpreter: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HarnessPathReport {
pub need: String,
pub path: Option<String>,
pub access: &'static str,
pub allowed_by: Option<String>,
pub status: &'static str,
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HarnessHostReport {
pub need: String,
pub allowed_by: Option<String>,
pub status: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HarnessEnvReport {
pub name: String,
pub status: &'static str,
}
const STATUS_GRANTED: &str = "granted";
const STATUS_MISSING: &str = "missing";
const STATUS_REFUSED: &str = "refused";
const STATUS_ALLOWED: &str = "allowed";
#[derive(Debug, Clone)]
pub struct HarnessBinaryInfo {
pub path: PathBuf,
pub interpreter: Option<PathBuf>,
pub launcher: Option<PathBuf>,
}
impl HarnessBinaryInfo {
#[must_use]
pub fn read(binary: &Path) -> Self {
let shebang = read_shebang(binary, &harness_path_env());
let (launcher, interpreter) = match shebang {
Some(shebang) => (shebang.launcher, Some(shebang.interpreter)),
None => (None, None),
};
Self {
path: binary.to_path_buf(),
interpreter,
launcher,
}
}
#[must_use]
pub fn exec_files(&self) -> Vec<PathBuf> {
std::iter::once(self.path.clone())
.chain(self.launcher.clone())
.chain(self.interpreter.clone())
.collect()
}
}
pub(crate) fn harness_path_env() -> String {
std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".to_string())
}
pub(crate) struct ResolvedHarness {
pub(crate) grants: HarnessGrants,
pub(crate) binary: Option<HarnessBinaryInfo>,
}
pub(crate) fn resolve_capsule_harness(
driver_ref: &str,
description: &Description,
binary: Option<&Path>,
policy: &CapabilityPolicy,
workdir: &Path,
) -> ResolvedHarness {
let binary = binary.map(HarnessBinaryInfo::read);
let home = crate::murmur_home::home_path().ok();
let murmur_home = home.as_ref().map(|home| home.join(".murmur"));
let context = HostPathContext {
home: home.as_deref(),
murmur_home: murmur_home.as_deref(),
workdir,
};
let grants = resolve_harness_grants(
driver_ref,
&description.harness_needs,
&policy.harness_paths,
&policy.network_allow,
&description.required_env,
&policy.env_allow,
binary.as_ref(),
&context,
);
ResolvedHarness { grants, binary }
}
pub(crate) fn validate_harness_needs(needs: &HarnessNeeds) -> Result<(), String> {
for need in &needs.paths {
validate_need_path(&need.path)?;
}
for host in &needs.hosts {
crate::network_policy::NetworkAllowRule::parse(host).map_err(|error| {
format!("names the host need {host:?}, which is not a capabilities.network.allow entry: {error}")
})?;
}
Ok(())
}
fn validate_need_path(path: &str) -> Result<(), String> {
if path.is_empty() {
return Err(format!("names the path need {path:?}, which is empty"));
}
match murmur_artifact::harness_path_shape_error(path) {
Some(reason) => Err(format!("names the path need {path:?}, which {reason}")),
None => Ok(()),
}
}
#[allow(clippy::too_many_arguments)]
pub fn resolve_harness_grants(
driver_ref: &str,
needs: &HarnessNeeds,
manifest_paths: &[murmur_artifact::HarnessPathEntry],
network_allow: &[String],
required_env: &[String],
env_allow: &[String],
binary: Option<&HarnessBinaryInfo>,
context: &HostPathContext<'_>,
) -> HarnessGrants {
let mut refusals: Vec<String> = Vec::new();
let mut granted_paths: Vec<HostPath> = Vec::new();
let home = context.home;
let manifest = fold_manifest_paths(manifest_paths, home);
let mut path_reports = Vec::with_capacity(needs.paths.len());
for need in &needs.paths {
let access = host_path_access(need.access);
let folded = match host_paths::expand_and_fold(&need.path, home) {
Ok(folded) => folded,
Err(reason) => {
refusals.push(format!("{} ({}): {reason}", need.path, access.wire_name()));
path_reports.push(HarnessPathReport {
need: need.path.clone(),
path: None,
access: access.wire_name(),
allowed_by: None,
status: STATUS_REFUSED,
reason: Some(reason),
});
continue;
}
};
match allow_path(&manifest, &folded, access) {
PathAllow::Allowed {
allowed_by,
entry_path,
} => {
let declared = DeclaredHostPath {
index: 0,
declared: need.path.clone(),
expanded: folded,
access,
};
let resolved =
host_paths::resolve_host_paths(std::slice::from_ref(&declared), context)
.and_then(|resolved| {
let host_path = resolved.into_iter().next().expect("one in, one out");
escapes_allowing_entry(&host_path, &entry_path, &allowed_by)
.map_or(Ok(host_path), Err)
});
match resolved {
Ok(host_path) => {
path_reports.push(HarnessPathReport {
need: need.path.clone(),
path: Some(host_path.path.display().to_string()),
access: access.wire_name(),
allowed_by: Some(allowed_by),
status: STATUS_GRANTED,
reason: None,
});
granted_paths.push(host_path);
}
Err(refusal) if refusal.missing => {
path_reports.push(HarnessPathReport {
need: need.path.clone(),
path: None,
access: access.wire_name(),
allowed_by: Some(allowed_by),
status: STATUS_MISSING,
reason: None,
});
}
Err(refusal) => {
let reason = refusal.reason.clone();
refusals.push(format!("{} ({}): {reason}", need.path, access.wire_name()));
path_reports.push(HarnessPathReport {
need: need.path.clone(),
path: refusal.resolved.map(|p| p.display().to_string()),
access: access.wire_name(),
allowed_by: Some(allowed_by),
status: STATUS_REFUSED,
reason: Some(reason),
});
}
}
}
PathAllow::NotAllowed { reason } => {
refusals.push(format!("{} ({}): {reason}", need.path, access.wire_name()));
path_reports.push(HarnessPathReport {
need: need.path.clone(),
path: None,
access: access.wire_name(),
allowed_by: None,
status: STATUS_REFUSED,
reason: Some(reason),
});
}
}
}
let network_rules =
crate::network_policy::parse_network_allow_rules(network_allow).unwrap_or_default();
let mut host_reports = Vec::with_capacity(needs.hosts.len());
for host in &needs.hosts {
match allow_host(host, network_allow, &network_rules) {
Some(allowed_by) => host_reports.push(HarnessHostReport {
need: host.clone(),
allowed_by: Some(allowed_by),
status: STATUS_ALLOWED,
}),
None => {
refusals.push(format!(
"{host}: no capabilities.network.allow entry admits it"
));
host_reports.push(HarnessHostReport {
need: host.clone(),
allowed_by: None,
status: STATUS_REFUSED,
});
}
}
}
let env = required_env
.iter()
.map(|name| HarnessEnvReport {
name: name.clone(),
status: if env_allow.iter().any(|allowed| allowed == name) {
STATUS_ALLOWED
} else {
STATUS_REFUSED
},
})
.collect();
let report = HarnessReport {
driver: driver_ref.to_string(),
binary: binary.map(|info| HarnessBinaryReport {
path: info.path.display().to_string(),
interpreter: info.interpreter.as_ref().map(|p| p.display().to_string()),
}),
paths: path_reports,
hosts: host_reports,
env,
unresolved: None,
};
let refusal =
(!refusals.is_empty()).then(|| crate::errors::RuntimeError::HarnessNeedsNotAllowed {
driver: driver_ref.to_string(),
refusals,
});
HarnessGrants {
report,
granted_paths,
refusal,
}
}
#[must_use]
pub fn unresolved_report(driver_ref: &str, reason: String) -> HarnessReport {
HarnessReport {
driver: driver_ref.to_string(),
binary: None,
paths: Vec::new(),
hosts: Vec::new(),
env: Vec::new(),
unresolved: Some(reason),
}
}
#[must_use]
pub fn render_harness_block(report: &HarnessReport) -> String {
let mut out = format!("Process harness ({})\n", report.driver);
if let Some(reason) = &report.unresolved {
out.push_str(&format!(" driver not read: {reason}\n"));
return out;
}
match &report.binary {
Some(binary) => {
out.push_str(&format!(" binary {}\n", binary.path));
if let Some(interpreter) = &binary.interpreter {
out.push_str(&format!(" interpreter {interpreter}\n"));
}
}
None => out.push_str(" binary <unresolved>\n"),
}
for path in &report.paths {
let line = match path.status {
STATUS_GRANTED => format!(
" path {} → {} ({}), allowed by {}",
path.need,
path.path.as_deref().unwrap_or("?"),
path.access,
path.allowed_by.as_deref().unwrap_or("?"),
),
STATUS_MISSING => format!(
" path {} ({}): missing on this host, allowed by {}",
path.need,
path.access,
path.allowed_by.as_deref().unwrap_or("?"),
),
_ => format!(
" path {} ({}): refused — {}",
path.need,
path.access,
path.reason.as_deref().unwrap_or(""),
),
};
out.push_str(&line);
out.push('\n');
}
for host in &report.hosts {
let line = match host.status {
STATUS_ALLOWED => format!(
" host {}, allowed by {}",
host.need,
host.allowed_by.as_deref().unwrap_or("?"),
),
_ => format!(" host {}: refused", host.need),
};
out.push_str(&line);
out.push('\n');
}
for env in &report.env {
out.push_str(&format!(" env {} ({})\n", env.name, env.status));
}
out
}
struct FoldedManifestPath {
declared: String,
expanded: PathBuf,
access: HostPathAccess,
}
fn fold_manifest_paths(
entries: &[murmur_artifact::HarnessPathEntry],
home: Option<&Path>,
) -> Vec<FoldedManifestPath> {
entries
.iter()
.filter_map(|entry| {
let expanded = host_paths::expand_and_fold(&entry.path, home).ok()?;
Some(FoldedManifestPath {
declared: entry.path.clone(),
expanded,
access: manifest_access(entry.access),
})
})
.collect()
}
enum PathAllow {
Allowed {
allowed_by: String,
entry_path: PathBuf,
},
NotAllowed {
reason: String,
},
}
fn escapes_allowing_entry(
host_path: &HostPath,
entry_path: &Path,
allowed_by: &str,
) -> Option<host_paths::HostPathRefusal> {
let refuse = |reason: String| host_paths::HostPathRefusal {
index: 0,
declared: host_path.declared.clone(),
resolved: Some(host_path.path.clone()),
reason,
missing: false,
};
match std::fs::canonicalize(entry_path) {
Ok(entry) if host_path.path.starts_with(&entry) => None,
Ok(entry) => Some(refuse(format!(
"resolves to {}, outside {} where the capabilities.harness.paths entry {allowed_by:?} \
that allows it resolves; a symlink on the way leads out of the granted folder",
host_path.path.display(),
entry.display(),
))),
Err(error) => Some(refuse(format!(
"the capabilities.harness.paths entry {allowed_by:?} that allows it cannot be \
resolved on this host: {error}"
))),
}
}
fn allow_path(
manifest: &[FoldedManifestPath],
folded: &Path,
need_access: HostPathAccess,
) -> PathAllow {
let mut shortfall: Option<&FoldedManifestPath> = None;
for entry in manifest {
if folded == entry.expanded || folded.starts_with(&entry.expanded) {
if access_at_least(entry.access, need_access) {
return PathAllow::Allowed {
allowed_by: entry.declared.clone(),
entry_path: entry.expanded.clone(),
};
}
shortfall.get_or_insert(entry);
}
}
let reason = match shortfall {
Some(entry) => format!(
"capabilities.harness.paths entry {:?} covers it at access {}, lower than the need's \
{}",
entry.declared,
entry.access.wire_name(),
need_access.wire_name()
),
None => "no capabilities.harness.paths entry names it or an ancestor of it".to_string(),
};
PathAllow::NotAllowed { reason }
}
fn access_at_least(have: HostPathAccess, want: HostPathAccess) -> bool {
matches!(
(have, want),
(HostPathAccess::ReadWrite, _) | (HostPathAccess::Read, HostPathAccess::Read)
)
}
fn allow_host(
need: &str,
network_allow: &[String],
rules: &[crate::network_policy::NetworkAllowRule],
) -> Option<String> {
let need_rule = crate::network_policy::NetworkAllowRule::parse(need).ok()?;
let need_ports: Vec<u16> = crate::egress_proxy::implied_tcp_ports(&need_rule).collect();
for (rule, declared) in rules.iter().zip(network_allow.iter()) {
if rule.host != need_rule.host {
continue;
}
let covers_every_port = need_ports.iter().all(|&port| {
crate::egress_proxy::implied_tcp_ports(rule).any(|allowed| allowed == port)
});
if covers_every_port {
return Some(declared.clone());
}
}
None
}
fn host_path_access(access: PathAccess) -> HostPathAccess {
match access {
PathAccess::Read => HostPathAccess::Read,
PathAccess::ReadWrite => HostPathAccess::ReadWrite,
}
}
fn manifest_access(access: murmur_artifact::HarnessPathAccess) -> HostPathAccess {
match access {
murmur_artifact::HarnessPathAccess::Read => HostPathAccess::Read,
murmur_artifact::HarnessPathAccess::ReadWrite => HostPathAccess::ReadWrite,
}
}
struct Shebang {
launcher: Option<PathBuf>,
interpreter: PathBuf,
}
const SHEBANG_MAX: u64 = 256;
fn read_shebang(binary: &Path, path_env: &str) -> Option<Shebang> {
use std::io::Read;
let mut head = Vec::with_capacity(SHEBANG_MAX as usize);
std::fs::File::open(binary)
.ok()?
.take(SHEBANG_MAX)
.read_to_end(&mut head)
.ok()?;
let line = head.strip_prefix(b"#!")?.split(|&b| b == b'\n').next()?;
let line = std::str::from_utf8(line).ok()?;
let mut parts = line.split_whitespace();
let first = parts.next()?;
let program = std::fs::canonicalize(first).ok()?;
if Path::new(first).file_name().and_then(|n| n.to_str()) == Some("env") {
let name = parts.next()?;
return Some(Shebang {
launcher: Some(program),
interpreter: resolve_on_path(name, path_env)?,
});
}
Some(Shebang {
launcher: None,
interpreter: program,
})
}
fn resolve_on_path(name: &str, path_env: &str) -> Option<PathBuf> {
if name.contains('/') {
return std::fs::canonicalize(name).ok();
}
for dir in path_env.split(':').filter(|d| !d.is_empty()) {
let candidate = Path::new(dir).join(name);
if candidate.is_file() {
return std::fs::canonicalize(&candidate).ok();
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::process_driver::PathNeed;
use murmur_artifact::{HarnessPathAccess, HarnessPathEntry};
fn needs(paths: &[(&str, PathAccess)], hosts: &[&str]) -> HarnessNeeds {
HarnessNeeds {
paths: paths
.iter()
.map(|(path, access)| PathNeed {
path: (*path).to_string(),
access: *access,
})
.collect(),
hosts: hosts.iter().map(|host| (*host).to_string()).collect(),
}
}
fn grant(path: &str, access: HarnessPathAccess) -> HarnessPathEntry {
HarnessPathEntry {
path: path.to_string(),
access,
}
}
struct Host {
_root: tempfile::TempDir,
home: PathBuf,
murmur_home: PathBuf,
workdir: PathBuf,
}
impl Host {
fn new() -> Self {
let root =
tempfile::tempdir_in(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target"))
.unwrap();
let home = std::fs::canonicalize(root.path()).unwrap().join("home");
let murmur_home = home.join(".murmur");
let workdir = home.join("project");
std::fs::create_dir_all(&murmur_home).unwrap();
std::fs::create_dir_all(&workdir).unwrap();
Self {
_root: root,
home,
murmur_home,
workdir,
}
}
fn context(&self) -> HostPathContext<'_> {
HostPathContext {
home: Some(&self.home),
murmur_home: Some(&self.murmur_home),
workdir: &self.workdir,
}
}
fn resolve(
&self,
needs: &HarnessNeeds,
manifest: &[HarnessPathEntry],
network_allow: &[&str],
) -> HarnessGrants {
let network_allow: Vec<String> = network_allow
.iter()
.map(|rule| (*rule).to_string())
.collect();
resolve_harness_grants(
"local/d@1.0.0",
needs,
manifest,
&network_allow,
&[],
&[],
None,
&self.context(),
)
}
}
fn refusal_text(grants: &HarnessGrants) -> String {
grants
.refusal
.as_ref()
.expect("the launch is refused")
.to_string()
}
#[test]
fn well_formed_and_empty_needs_pass_validation() {
assert!(validate_harness_needs(&needs(&[], &[])).is_ok());
assert!(validate_harness_needs(&needs(
&[
("~/.claude", PathAccess::ReadWrite),
("/opt/harness/", PathAccess::Read),
],
&["api.anthropic.com", "example.invalid:443"],
))
.is_ok());
}
#[test]
fn a_malformed_need_is_refused_naming_it() {
for (path, expected) in [
("", "is empty"),
("relative/dir", "is relative"),
("~", "names the whole home folder"),
("~/", "names the whole home folder"),
("/", "names /, the whole host filesystem"),
("~/a/../b", "has a . or .. segment"),
("/opt/./x", "has a . or .. segment"),
("/opt//x", "has an empty path segment"),
] {
let reason = validate_harness_needs(&needs(&[(path, PathAccess::Read)], &[]))
.expect_err("a malformed path need is refused");
assert!(reason.contains(&format!("{path:?}")), "{path}: {reason}");
assert!(reason.contains(expected), "{path}: {reason}");
}
let reason = validate_harness_needs(&needs(&[], &["http://example.com/x"]))
.expect_err("a host the network.allow parser refuses is refused");
assert!(reason.contains("\"http://example.com/x\""), "{reason}");
assert!(
reason.contains("not a capabilities.network.allow entry"),
"{reason}"
);
}
#[test]
fn an_allowed_need_under_home_is_granted_at_its_own_path_and_access() {
let host = Host::new();
std::fs::create_dir_all(host.home.join("state/sessions")).unwrap();
let grants = host.resolve(
&needs(&[("~/state/sessions", PathAccess::ReadWrite)], &[]),
&[grant("~/state", HarnessPathAccess::ReadWrite)],
&[],
);
assert!(grants.refusal.is_none(), "{:?}", grants.refusal);
assert_eq!(grants.granted_paths.len(), 1);
assert_eq!(
grants.granted_paths[0].path,
host.home.join("state/sessions")
);
assert_eq!(grants.granted_paths[0].access, HostPathAccess::ReadWrite);
let row = &grants.report.paths[0];
assert_eq!(row.status, "granted");
assert_eq!(row.allowed_by.as_deref(), Some("~/state"));
assert_eq!(row.access, "read-write");
}
#[test]
fn an_entry_below_the_need_access_names_the_shortfall() {
let host = Host::new();
std::fs::create_dir_all(host.home.join("state")).unwrap();
let grants = host.resolve(
&needs(&[("~/state", PathAccess::ReadWrite)], &[]),
&[grant("~/state", HarnessPathAccess::Read)],
&[],
);
let message = refusal_text(&grants);
assert!(message.contains("'local/d@1.0.0'"), "{message}");
assert!(message.contains("~/state (read-write)"), "{message}");
assert!(
message.contains(
"entry \"~/state\" covers it at access read, lower than the need's read-write"
),
"{message}"
);
assert!(grants.granted_paths.is_empty());
assert_eq!(grants.report.paths[0].status, "refused");
}
#[test]
fn every_disallowed_need_is_named_in_one_refusal() {
let host = Host::new();
let grants = host.resolve(
&needs(
&[("~/a", PathAccess::Read), ("/srv/b", PathAccess::ReadWrite)],
&["example.invalid:443"],
),
&[],
&[],
);
let message = refusal_text(&grants);
for need in [
"~/a (read)",
"/srv/b (read-write)",
"example.invalid:443: no capabilities.network.allow",
] {
assert!(message.contains(need), "{need}: {message}");
}
assert!(
!message.contains('\n'),
"one line, so doctor keeps every need: {message}"
);
}
#[test]
fn an_allowed_need_absent_on_this_host_is_missing_and_never_refuses() {
let host = Host::new();
let grants = host.resolve(
&needs(&[("~/absent", PathAccess::ReadWrite)], &[]),
&[grant("~/absent", HarnessPathAccess::ReadWrite)],
&[],
);
assert!(grants.refusal.is_none(), "{:?}", grants.refusal);
assert!(grants.granted_paths.is_empty());
let row = &grants.report.paths[0];
assert_eq!(row.status, "missing");
assert_eq!(row.path, None);
assert_eq!(row.allowed_by.as_deref(), Some("~/absent"));
}
#[test]
fn a_read_write_need_over_murmurs_own_home_is_refused_and_a_sibling_is_not() {
let host = Host::new();
std::fs::create_dir_all(host.home.join("sibling")).unwrap();
let grants = host.resolve(
&needs(
&[
("~/.murmur", PathAccess::ReadWrite),
("~/sibling", PathAccess::ReadWrite),
],
&[],
),
&[
grant("~/.murmur", HarnessPathAccess::ReadWrite),
grant("~/sibling", HarnessPathAccess::ReadWrite),
],
&[],
);
let message = refusal_text(&grants);
assert!(message.contains("~/.murmur (read-write)"), "{message}");
assert!(
message.contains("read-write over murmur's own home"),
"{message}"
);
assert!(!message.contains("~/sibling"), "{message}");
assert_eq!(grants.report.paths[1].status, "granted");
}
#[test]
fn a_need_that_a_symlink_leads_outside_its_entry_is_refused() {
let host = Host::new();
let parent = host.home.join("parent");
let elsewhere = host.home.join("elsewhere");
std::fs::create_dir_all(&parent).unwrap();
std::fs::create_dir_all(&elsewhere).unwrap();
std::os::unix::fs::symlink(&elsewhere, parent.join("sessions")).unwrap();
let grants = host.resolve(
&needs(&[("~/parent/sessions", PathAccess::ReadWrite)], &[]),
&[grant("~/parent", HarnessPathAccess::ReadWrite)],
&[],
);
let message = refusal_text(&grants);
assert!(
message.contains("~/parent/sessions (read-write)"),
"{message}"
);
assert!(
message.contains("a symlink on the way leads out"),
"{message}"
);
assert!(
!message.contains(" "),
"one space between words: {message}"
);
assert!(grants.granted_paths.is_empty());
let row = &grants.report.paths[0];
assert_eq!(row.status, "refused");
assert_eq!(row.path.as_deref(), Some(elsewhere.to_str().unwrap()));
let inside = parent.join("real");
std::fs::create_dir_all(&inside).unwrap();
std::os::unix::fs::symlink(&inside, parent.join("linked")).unwrap();
let grants = host.resolve(
&needs(&[("~/parent/linked", PathAccess::ReadWrite)], &[]),
&[grant("~/parent", HarnessPathAccess::ReadWrite)],
&[],
);
assert!(grants.refusal.is_none(), "{:?}", grants.refusal);
assert_eq!(grants.granted_paths[0].path, inside);
}
#[test]
fn a_host_need_is_allowed_only_when_every_port_it_spans_is_admitted() {
let host = Host::new();
let resolve = |need: &str, allow: &[&str]| {
host.resolve(&needs(&[], &[need]), &[], allow).report.hosts[0].clone()
};
let row = resolve("example.invalid:443", &["example.invalid"]);
assert_eq!(
(row.status, row.allowed_by.as_deref()),
("allowed", Some("example.invalid"))
);
let row = resolve("example.invalid:443", &["example.invalid:443"]);
assert_eq!(row.status, "allowed");
assert_eq!(
resolve("example.invalid", &["example.invalid:443"]).status,
"refused"
);
assert_eq!(
resolve("example.invalid:8443", &["example.invalid"]).status,
"refused"
);
assert_eq!(
resolve("example.invalid:443", &["other.invalid"]).status,
"refused"
);
}
#[test]
fn a_shebang_names_its_interpreter_and_env_both() {
let host = Host::new();
let script = host.home.join("harness");
std::fs::write(&script, "#!/bin/sh\necho hi\n").unwrap();
let info = HarnessBinaryInfo::read(&script);
let sh = std::fs::canonicalize("/bin/sh").unwrap();
assert_eq!(info.interpreter.as_ref(), Some(&sh));
assert_eq!(info.launcher, None);
assert_eq!(info.exec_files(), vec![script.clone(), sh.clone()]);
std::fs::write(&script, "#!/usr/bin/env sh\necho hi\n").unwrap();
let info = HarnessBinaryInfo::read(&script);
let env = std::fs::canonicalize("/usr/bin/env").unwrap();
assert_eq!(info.launcher.as_ref(), Some(&env));
assert_eq!(info.exec_files().len(), 3, "{:?}", info.exec_files());
assert!(info.exec_files().contains(&env));
std::fs::write(&script, b"\x7fELF\x02\x01").unwrap();
let info = HarnessBinaryInfo::read(&script);
assert_eq!((info.interpreter, info.launcher), (None, None));
}
}