use std::collections::BTreeMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::evidence::ExecutionIdentity;
use super::frozen::FrozenSpec;
use super::model::{Category, Verdict};
use super::registry::Scenario;
use crate::policy::Tier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SecurityCapability {
FilesystemIsolation,
NetworkIsolation,
ProcessIsolation,
ProcessTreeContainment,
ResourceLimits,
SyscallRestriction,
ExecutionRootIsolation,
HostEvidence,
}
impl SecurityCapability {
pub fn label(self) -> &'static str {
match self {
SecurityCapability::FilesystemIsolation => "filesystem",
SecurityCapability::NetworkIsolation => "network",
SecurityCapability::ProcessIsolation => "process",
SecurityCapability::ProcessTreeContainment => "tree",
SecurityCapability::ResourceLimits => "resources",
SecurityCapability::SyscallRestriction => "syscalls",
SecurityCapability::ExecutionRootIsolation => "exec-root",
SecurityCapability::HostEvidence => "host-evidence",
}
}
pub fn all() -> [SecurityCapability; 8] {
[
SecurityCapability::FilesystemIsolation,
SecurityCapability::NetworkIsolation,
SecurityCapability::ProcessIsolation,
SecurityCapability::ProcessTreeContainment,
SecurityCapability::ResourceLimits,
SecurityCapability::SyscallRestriction,
SecurityCapability::ExecutionRootIsolation,
SecurityCapability::HostEvidence,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EnforcementState {
Requested,
Configured,
Enforced,
Verified,
Unsupported,
Failed,
}
impl EnforcementState {
pub fn label(self) -> &'static str {
match self {
EnforcementState::Requested => "requested",
EnforcementState::Configured => "configured",
EnforcementState::Enforced => "enforced",
EnforcementState::Verified => "verified",
EnforcementState::Unsupported => "unsupported",
EnforcementState::Failed => "failed",
}
}
pub fn is_enforced(self) -> bool {
matches!(
self,
EnforcementState::Enforced | EnforcementState::Verified
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PreparationFailureKind {
UnsupportedOnPlatform,
PlatformUnavailable,
SpawnRefused,
VerificationUnavailable,
}
impl PreparationFailureKind {
pub fn label(self) -> &'static str {
match self {
PreparationFailureKind::UnsupportedOnPlatform => "unsupported-on-platform",
PreparationFailureKind::PlatformUnavailable => "platform-unavailable",
PreparationFailureKind::SpawnRefused => "spawn-refused",
PreparationFailureKind::VerificationUnavailable => "verification-unavailable",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BackendKind {
Direct,
Linux,
Macos,
Windows,
}
impl BackendKind {
pub fn label(self) -> &'static str {
match self {
BackendKind::Direct => "direct",
BackendKind::Linux => "linux",
BackendKind::Macos => "macos",
BackendKind::Windows => "windows",
}
}
pub fn all() -> [BackendKind; 4] {
[
BackendKind::Direct,
BackendKind::Linux,
BackendKind::Macos,
BackendKind::Windows,
]
}
pub fn current_platform() -> BackendKind {
#[cfg(target_os = "linux")]
{
BackendKind::Linux
}
#[cfg(target_os = "macos")]
{
BackendKind::Macos
}
#[cfg(target_os = "windows")]
{
BackendKind::Windows
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
BackendKind::Direct
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CanonicalPolicy {
pub scenario_id: String,
pub registry_hash: String,
pub session_nonce: String,
pub frozen_hash: String,
pub net_mode: String,
pub tier: String,
pub backend_hint: String,
pub argv: Vec<String>,
pub env: BTreeMap<String, String>,
pub cwd: PathBuf,
pub policy_bytes: Vec<u8>,
pub policy_hash: String,
}
impl CanonicalPolicy {
pub fn from_frozen(spec: &FrozenSpec) -> Self {
let policy_hash = {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&spec.policy_bytes);
super::frozen::hex_encode(&hasher.finalize())
};
CanonicalPolicy {
scenario_id: spec.scenario_id.clone(),
registry_hash: spec.registry_hash.clone(),
session_nonce: spec.nonce.clone(),
frozen_hash: spec.hash(),
net_mode: spec.net_mode.clone(),
tier: spec.tier.clone(),
backend_hint: spec.backend.clone(),
argv: spec.argv.clone(),
env: spec.env.clone(),
cwd: spec.cwd.clone(),
policy_bytes: spec.policy_bytes.clone(),
policy_hash,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityRecord {
pub capability: SecurityCapability,
pub requested: bool,
pub state: EnforcementState,
pub failure: Option<PreparationFailureKind>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnforcementReport {
pub backend: BackendKind,
pub scenario_id: String,
pub session_nonce: String,
pub registry_hash: String,
pub frozen_hash: String,
pub policy_hash: String,
pub preparation_ok: bool,
pub records: Vec<CapabilityRecord>,
}
impl EnforcementReport {
pub fn build(
backend: BackendKind,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
states: &BTreeMap<SecurityCapability, EnforcementState>,
failures: &BTreeMap<SecurityCapability, PreparationFailureKind>,
preparation_ok: bool,
) -> Self {
let mut records = Vec::new();
for cap in SecurityCapability::all() {
let state = states
.get(&cap)
.copied()
.unwrap_or(EnforcementState::Unsupported);
let failure = failures.get(&cap).copied();
records.push(CapabilityRecord {
capability: cap,
requested: true,
state,
failure,
});
}
EnforcementReport {
backend,
scenario_id: identity.scenario_id.clone(),
session_nonce: identity.session_nonce.clone(),
registry_hash: identity.registry_hash.clone(),
frozen_hash: identity.frozen_hash.clone(),
policy_hash: policy.policy_hash.clone(),
preparation_ok,
records,
}
}
pub fn state(&self, capability: SecurityCapability) -> EnforcementState {
self.records
.iter()
.find(|r| r.capability == capability)
.map(|r| r.state)
.unwrap_or(EnforcementState::Unsupported)
}
pub fn is_enforced(&self, capability: SecurityCapability) -> bool {
self.preparation_ok && self.state(capability).is_enforced()
}
pub fn requested(&self) -> Vec<SecurityCapability> {
self.records
.iter()
.filter(|r| r.requested)
.map(|r| r.capability)
.collect()
}
pub fn enforced(&self) -> Vec<SecurityCapability> {
SecurityCapability::all()
.into_iter()
.filter(|c| self.is_enforced(*c))
.collect()
}
pub fn unsupported(&self) -> Vec<SecurityCapability> {
self.records
.iter()
.filter(|r| r.state == EnforcementState::Unsupported)
.map(|r| r.capability)
.collect()
}
pub fn failed(&self) -> Vec<(SecurityCapability, Option<PreparationFailureKind>)> {
self.records
.iter()
.filter(|r| r.state == EnforcementState::Failed)
.map(|r| (r.capability, r.failure))
.collect()
}
pub fn allows_pass(&self, required: &[SecurityCapability]) -> bool {
if !self.preparation_ok {
return false;
}
required.iter().all(|c| self.is_enforced(*c))
}
pub fn binds_identity(&self, identity: &ExecutionIdentity) -> bool {
self.scenario_id == identity.scenario_id
&& self.session_nonce == identity.session_nonce
&& self.registry_hash == identity.registry_hash
&& self.frozen_hash == identity.frozen_hash
}
pub fn render_deterministic(&self) -> String {
let mut parts = vec![format!("backend={}", self.backend.label())];
for record in &self.records {
parts.push(format!(
"{}={}",
record.capability.label(),
record.state.label()
));
}
parts.push(format!("preparation_ok={}", self.preparation_ok));
parts.join("|")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnforcementFact {
pub capability: SecurityCapability,
pub state: EnforcementState,
pub backend: BackendKind,
}
#[derive(Debug, Clone, Default)]
pub struct PrepareContext {
pub extra_rw: Vec<std::path::PathBuf>,
}
#[derive(Debug, Clone)]
pub struct ChildEnforcementPlan {
pub landlock: bool,
pub exec_root: std::path::PathBuf,
pub extra_rw: Vec<std::path::PathBuf>,
pub system_ro: Vec<std::path::PathBuf>,
pub net_deny: bool,
pub harden_syscalls: bool,
pub rlimit_as: Option<u64>,
pub rlimit_nproc: Option<u64>,
pub rlimit_cpu: Option<u64>,
pub rlimit_fsize: Option<u64>,
pub new_pgroup: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostVerification {
pub seccomp_filter: bool,
pub no_new_privs: bool,
pub pgroup_separate: bool,
pub rlimit_as_ok: bool,
pub rlimit_nproc_ok: bool,
pub rlimit_cpu_ok: bool,
pub rlimit_fsize_ok: bool,
pub subreaper_ok: bool,
pub win_in_job: bool,
pub win_kill_on_close: bool,
pub win_low_integrity: bool,
pub win_job_ceiling: bool,
}
impl HostVerification {
pub fn none() -> Self {
HostVerification {
seccomp_filter: false,
no_new_privs: false,
pgroup_separate: false,
rlimit_as_ok: false,
rlimit_nproc_ok: false,
rlimit_cpu_ok: false,
rlimit_fsize_ok: false,
subreaper_ok: false,
win_in_job: false,
win_kill_on_close: false,
win_low_integrity: false,
win_job_ceiling: false,
}
}
pub fn all_observed(&self) -> bool {
self.seccomp_filter
&& self.no_new_privs
&& self.pgroup_separate
&& self.rlimit_as_ok
&& self.rlimit_nproc_ok
&& self.rlimit_cpu_ok
&& self.rlimit_fsize_ok
&& self.subreaper_ok
}
}
pub trait SandboxBackend: Send {
fn kind(&self) -> BackendKind;
fn name(&self) -> &'static str;
fn supports(&self, capability: SecurityCapability) -> bool;
fn supported_capabilities(&self) -> Vec<SecurityCapability> {
SecurityCapability::all()
.into_iter()
.filter(|c| self.supports(*c))
.collect()
}
fn prepare(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
) -> EnforcementReport;
fn prepare_with_context(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
ctx: &PrepareContext,
) -> EnforcementReport {
let _ = ctx;
self.prepare(policy, identity)
}
fn pre_exec_plan(&self) -> Option<ChildEnforcementPlan> {
None
}
fn note_spawned(&mut self, _pid: u32) {}
fn note_host_verified(&mut self, _verification: &HostVerification) {}
fn note_failed(&mut self, _kind: PreparationFailureKind) {}
fn note_tree_clean(&mut self, _clean: bool) {}
fn note_diagnostic(&mut self, _diag: String) {}
fn diagnostic(&self) -> Option<String> {
None
}
fn restrict_tier(&mut self, _tier: Option<Tier>) {}
fn enforcement(&self) -> Option<&EnforcementReport>;
fn is_enforced(&self, capability: SecurityCapability) -> bool {
self.enforcement()
.map(|r| r.is_enforced(capability))
.unwrap_or(false)
}
fn enforcement_evidence(&self) -> Vec<EnforcementFact> {
match self.enforcement() {
Some(report) => report
.records
.iter()
.map(|r| EnforcementFact {
capability: r.capability,
state: r.state,
backend: report.backend,
})
.collect(),
None => Vec::new(),
}
}
fn teardown(&mut self);
}
#[derive(Debug, Clone, Default)]
pub struct DirectBackend {
report: Option<EnforcementReport>,
}
impl DirectBackend {
pub fn new() -> Self {
DirectBackend { report: None }
}
}
impl SandboxBackend for DirectBackend {
fn kind(&self) -> BackendKind {
BackendKind::Direct
}
fn name(&self) -> &'static str {
"direct-exec (no sandbox; plumbing only)"
}
fn supports(&self, capability: SecurityCapability) -> bool {
matches!(capability, SecurityCapability::HostEvidence)
}
fn prepare(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
) -> EnforcementReport {
let mut states = BTreeMap::new();
for cap in SecurityCapability::all() {
let state = if cap == SecurityCapability::HostEvidence {
EnforcementState::Enforced
} else {
EnforcementState::Unsupported
};
states.insert(cap, state);
}
let report = EnforcementReport::build(
BackendKind::Direct,
policy,
identity,
&states,
&BTreeMap::new(),
true,
);
self.report = Some(report.clone());
report
}
fn enforcement(&self) -> Option<&EnforcementReport> {
self.report.as_ref()
}
fn teardown(&mut self) {
self.report = None;
}
}
#[derive(Debug, Clone, Default)]
pub struct LinuxBackend {
report: Option<EnforcementReport>,
plan: Option<ChildEnforcementPlan>,
tree_diag: Option<String>,
subreaper_prepare: Option<String>,
tier: Option<Tier>,
}
impl LinuxBackend {
pub fn new() -> Self {
LinuxBackend {
report: None,
plan: None,
tree_diag: None,
subreaper_prepare: None,
tier: None,
}
}
fn promote_configured_to_enforced(&mut self) {
if let Some(report) = self.report.as_mut() {
for record in &mut report.records {
if record.state == EnforcementState::Configured {
record.state = EnforcementState::Enforced;
}
}
}
}
fn fail_installed(&mut self, kind: PreparationFailureKind) {
if let Some(report) = self.report.as_mut() {
for record in &mut report.records {
match record.state {
EnforcementState::Configured
| EnforcementState::Enforced
| EnforcementState::Verified => {
record.state = EnforcementState::Failed;
record.failure = Some(kind);
}
_ => {}
}
}
report.preparation_ok = false;
}
}
pub fn restrict_to_seccomp_tier(&mut self, tier: Option<Tier>) {
self.tier = tier;
if tier == Some(Tier::Seccomp) {
self.restrict_seccomp_records();
}
}
fn restrict_seccomp_records(&mut self) {
if let Some(report) = self.report.as_mut() {
for record in &mut report.records {
if matches!(
record.capability,
SecurityCapability::FilesystemIsolation
| SecurityCapability::ExecutionRootIsolation
) {
record.state = EnforcementState::Unsupported;
record.failure = None;
}
}
}
}
}
impl SandboxBackend for LinuxBackend {
fn kind(&self) -> BackendKind {
BackendKind::Linux
}
fn name(&self) -> &'static str {
"linux (landlock+seccomp+rlimit+pgroup; no userns)"
}
fn supports(&self, capability: SecurityCapability) -> bool {
#[cfg(target_os = "linux")]
{
match capability {
SecurityCapability::FilesystemIsolation
| SecurityCapability::NetworkIsolation
| SecurityCapability::ProcessIsolation
| SecurityCapability::ProcessTreeContainment
| SecurityCapability::ResourceLimits
| SecurityCapability::SyscallRestriction
| SecurityCapability::ExecutionRootIsolation
| SecurityCapability::HostEvidence => true,
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = capability;
false
}
}
fn prepare(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
) -> EnforcementReport {
self.prepare_with_context(policy, identity, &PrepareContext::default())
}
fn prepare_with_context(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
ctx: &PrepareContext,
) -> EnforcementReport {
self.plan = None;
#[cfg(not(target_os = "linux"))]
{
let _ = ctx;
let states: BTreeMap<SecurityCapability, EnforcementState> = SecurityCapability::all()
.into_iter()
.map(|c| (c, EnforcementState::Unsupported))
.collect();
let report = EnforcementReport::build(
BackendKind::Linux,
policy,
identity,
&states,
&BTreeMap::new(),
true,
);
self.report = Some(report.clone());
if self.tier == Some(Tier::Seccomp) {
self.restrict_seccomp_records();
}
self.report.clone().unwrap_or(report)
}
#[cfg(target_os = "linux")]
{
self.prepare_linux(policy, identity, ctx)
}
}
fn pre_exec_plan(&self) -> Option<ChildEnforcementPlan> {
self.plan.clone()
}
fn note_spawned(&mut self, _pid: u32) {
self.promote_configured_to_enforced();
}
fn note_host_verified(&mut self, verification: &HostVerification) {
let Some(report) = self.report.as_mut() else {
return;
};
let mut verified = |cap: SecurityCapability, observed: bool| {
if observed {
if let Some(record) = report.records.iter_mut().find(|r| r.capability == cap) {
if record.state == EnforcementState::Enforced {
record.state = EnforcementState::Verified;
}
}
}
};
verified(
SecurityCapability::SyscallRestriction,
verification.seccomp_filter,
);
verified(
SecurityCapability::ProcessIsolation,
verification.no_new_privs && verification.pgroup_separate,
);
verified(
SecurityCapability::ResourceLimits,
verification.rlimit_as_ok
&& verification.rlimit_nproc_ok
&& verification.rlimit_cpu_ok
&& verification.rlimit_fsize_ok,
);
}
fn note_failed(&mut self, kind: PreparationFailureKind) {
self.fail_installed(kind);
}
fn note_tree_clean(&mut self, clean: bool) {
let Some(report) = self.report.as_mut() else {
return;
};
if let Some(record) = report
.records
.iter_mut()
.find(|r| r.capability == SecurityCapability::ProcessTreeContainment)
{
match record.state {
EnforcementState::Enforced if clean => {
record.state = EnforcementState::Verified;
}
EnforcementState::Enforced | EnforcementState::Configured if !clean => {
record.state = EnforcementState::Failed;
record.failure = Some(PreparationFailureKind::VerificationUnavailable);
}
_ => {}
}
}
}
fn note_diagnostic(&mut self, diag: String) {
let diag = match &self.subreaper_prepare {
Some(st) => format!("prepare-subreaper={st} {diag}"),
None => diag,
};
self.tree_diag = Some(diag);
}
fn diagnostic(&self) -> Option<String> {
self.tree_diag.clone()
}
fn restrict_tier(&mut self, tier: Option<Tier>) {
self.restrict_to_seccomp_tier(tier);
}
fn enforcement(&self) -> Option<&EnforcementReport> {
self.report.as_ref()
}
fn teardown(&mut self) {
self.report = None;
self.plan = None;
self.tree_diag = None;
self.subreaper_prepare = None;
self.tier = None;
}
}
#[cfg(target_os = "linux")]
impl LinuxBackend {
fn prepare_linux(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
ctx: &PrepareContext,
) -> EnforcementReport {
self.subreaper_prepare = Some(match crate::multi::isolation::set_subreaper() {
Ok(()) => {
format!(
"ok/subreaper={}",
super::linux_enforce::is_child_subreaper()
)
}
Err(e) => format!("ERR:{e:?}"),
});
let landlock_ok = crate::sandbox::linux::landlock::abi_version().is_some();
let seccomp_ok = crate::sandbox::linux::seccomp_netblock::probe_available();
let net_off = policy.net_mode == "off";
let mut states = BTreeMap::new();
let mut set = |cap: SecurityCapability, ok: bool| {
states.insert(
cap,
if ok {
EnforcementState::Configured
} else {
EnforcementState::Unsupported
},
);
};
set(SecurityCapability::FilesystemIsolation, landlock_ok);
set(SecurityCapability::ExecutionRootIsolation, landlock_ok);
set(SecurityCapability::NetworkIsolation, net_off && seccomp_ok);
set(SecurityCapability::SyscallRestriction, seccomp_ok);
set(SecurityCapability::ProcessIsolation, true);
set(SecurityCapability::ProcessTreeContainment, true);
set(SecurityCapability::ResourceLimits, true);
states.insert(SecurityCapability::HostEvidence, EnforcementState::Enforced);
let system_ro: Vec<std::path::PathBuf> = if landlock_ok {
super::linux_enforce::SYSTEM_ROOTS
.iter()
.map(std::path::PathBuf::from)
.collect()
} else {
Vec::new()
};
self.plan = Some(ChildEnforcementPlan {
landlock: landlock_ok,
exec_root: policy.cwd.clone(),
extra_rw: ctx.extra_rw.clone(),
system_ro,
net_deny: net_off,
harden_syscalls: seccomp_ok,
rlimit_as: Some(super::linux_enforce::DEFAULT_RLIMIT_AS_BYTES),
rlimit_nproc: Some(super::linux_enforce::DEFAULT_RLIMIT_NPROC),
rlimit_cpu: Some(super::linux_enforce::DEFAULT_RLIMIT_CPU_SECS),
rlimit_fsize: Some(super::linux_enforce::DEFAULT_RLIMIT_FSIZE_BYTES),
new_pgroup: true,
});
let report = EnforcementReport::build(
BackendKind::Linux,
policy,
identity,
&states,
&BTreeMap::new(),
true,
);
self.report = Some(report.clone());
if self.tier == Some(Tier::Seccomp) {
self.restrict_seccomp_records();
}
self.report.clone().unwrap_or(report)
}
}
#[derive(Debug, Clone, Default)]
pub struct MacosBackend {
report: Option<EnforcementReport>,
tree_diag: Option<String>,
}
impl MacosBackend {
pub fn new() -> Self {
MacosBackend {
report: None,
tree_diag: None,
}
}
fn promote_configured_to_enforced(&mut self) {
if let Some(report) = self.report.as_mut() {
for record in &mut report.records {
if record.state == EnforcementState::Configured {
record.state = EnforcementState::Enforced;
}
}
}
}
fn fail_installed(&mut self, kind: PreparationFailureKind) {
if let Some(report) = self.report.as_mut() {
for record in &mut report.records {
match record.state {
EnforcementState::Configured
| EnforcementState::Enforced
| EnforcementState::Verified => {
record.state = EnforcementState::Failed;
record.failure = Some(kind);
}
_ => {}
}
}
report.preparation_ok = false;
}
}
}
impl SandboxBackend for MacosBackend {
fn kind(&self) -> BackendKind {
BackendKind::Macos
}
fn name(&self) -> &'static str {
"macos (seatbelt write+net-off+rlimit+pgroup; no seccomp, reads broad)"
}
fn supports(&self, capability: SecurityCapability) -> bool {
#[cfg(target_os = "macos")]
{
matches!(
capability,
SecurityCapability::FilesystemIsolation
| SecurityCapability::NetworkIsolation
| SecurityCapability::ProcessIsolation
| SecurityCapability::ProcessTreeContainment
| SecurityCapability::ResourceLimits
| SecurityCapability::HostEvidence
)
}
#[cfg(not(target_os = "macos"))]
{
let _ = capability;
false
}
}
fn prepare(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
) -> EnforcementReport {
self.prepare_with_context(policy, identity, &PrepareContext::default())
}
fn prepare_with_context(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
ctx: &PrepareContext,
) -> EnforcementReport {
let _ = ctx;
self.tree_diag = None;
#[cfg(not(target_os = "macos"))]
{
let states: BTreeMap<SecurityCapability, EnforcementState> = SecurityCapability::all()
.into_iter()
.map(|c| (c, EnforcementState::Unsupported))
.collect();
let report = EnforcementReport::build(
BackendKind::Macos,
policy,
identity,
&states,
&BTreeMap::new(),
true,
);
self.report = Some(report.clone());
report
}
#[cfg(target_os = "macos")]
{
if policy.net_mode != "off" {
let mut states: BTreeMap<SecurityCapability, EnforcementState> =
SecurityCapability::all()
.into_iter()
.map(|c| (c, EnforcementState::Unsupported))
.collect();
states.insert(
SecurityCapability::NetworkIsolation,
EnforcementState::Failed,
);
let mut failures = BTreeMap::new();
failures.insert(
SecurityCapability::NetworkIsolation,
PreparationFailureKind::UnsupportedOnPlatform,
);
let report = EnforcementReport::build(
BackendKind::Macos,
policy,
identity,
&states,
&failures,
false,
);
self.report = Some(report.clone());
return report;
}
let mut states = BTreeMap::new();
let mut set = |cap: SecurityCapability, ok: bool| {
states.insert(
cap,
if ok {
EnforcementState::Configured
} else {
EnforcementState::Unsupported
},
);
};
set(SecurityCapability::FilesystemIsolation, true);
set(SecurityCapability::NetworkIsolation, true);
set(SecurityCapability::ProcessIsolation, true);
set(SecurityCapability::ProcessTreeContainment, true);
set(SecurityCapability::ResourceLimits, true);
set(SecurityCapability::SyscallRestriction, false);
set(SecurityCapability::ExecutionRootIsolation, false);
states.insert(SecurityCapability::HostEvidence, EnforcementState::Enforced);
let report = EnforcementReport::build(
BackendKind::Macos,
policy,
identity,
&states,
&BTreeMap::new(),
true,
);
self.report = Some(report.clone());
report
}
}
fn note_spawned(&mut self, _pid: u32) {
self.promote_configured_to_enforced();
}
fn note_host_verified(&mut self, verification: &HostVerification) {
let Some(report) = self.report.as_mut() else {
return;
};
if verification.pgroup_separate {
if let Some(record) = report
.records
.iter_mut()
.find(|r| r.capability == SecurityCapability::ProcessIsolation)
{
if record.state == EnforcementState::Enforced {
record.state = EnforcementState::Verified;
}
}
}
}
fn note_failed(&mut self, kind: PreparationFailureKind) {
self.fail_installed(kind);
}
fn note_tree_clean(&mut self, clean: bool) {
let Some(report) = self.report.as_mut() else {
return;
};
if let Some(record) = report
.records
.iter_mut()
.find(|r| r.capability == SecurityCapability::ProcessTreeContainment)
{
match record.state {
EnforcementState::Enforced if clean => {
record.state = EnforcementState::Verified;
}
EnforcementState::Enforced | EnforcementState::Configured if !clean => {
record.state = EnforcementState::Failed;
record.failure = Some(PreparationFailureKind::VerificationUnavailable);
}
_ => {}
}
}
}
fn note_diagnostic(&mut self, diag: String) {
self.tree_diag = Some(diag);
}
fn diagnostic(&self) -> Option<String> {
self.tree_diag.clone()
}
fn enforcement(&self) -> Option<&EnforcementReport> {
self.report.as_ref()
}
fn teardown(&mut self) {
self.report = None;
self.tree_diag = None;
}
}
#[derive(Debug, Clone, Default)]
pub struct WindowsBackend {
report: Option<EnforcementReport>,
spawned_pid: Option<u32>,
diagnostic: Option<String>,
}
impl WindowsBackend {
pub fn new() -> Self {
WindowsBackend {
report: None,
spawned_pid: None,
diagnostic: None,
}
}
fn prepare_with_facts(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
facts: &super::windows_enforce::ProbeFacts,
) -> EnforcementReport {
let (states, failures, preparation_ok) = super::windows_enforce::states_for_facts(facts);
let report = EnforcementReport::build(
BackendKind::Windows,
policy,
identity,
&states,
&failures,
preparation_ok,
);
self.spawned_pid = None;
self.diagnostic = Some(format!(
"win prepare ok={preparation_ok} as-user={} exec-root=unsupported syscalls=unsupported",
facts.experimental_as_user
));
self.report = Some(report.clone());
report
}
fn promote_configured(&mut self, capability: SecurityCapability) {
if let Some(report) = self.report.as_mut() {
if let Some(record) = report
.records
.iter_mut()
.find(|r| r.capability == capability)
{
if record.state == EnforcementState::Configured {
record.state = EnforcementState::Enforced;
}
}
}
}
}
impl SandboxBackend for WindowsBackend {
fn kind(&self) -> BackendKind {
BackendKind::Windows
}
fn name(&self) -> &'static str {
"windows (job kill-on-close + appcontainer process sandbox; no syscall filter)"
}
fn supports(&self, capability: SecurityCapability) -> bool {
#[cfg(target_os = "windows")]
{
matches!(
capability,
SecurityCapability::FilesystemIsolation
| SecurityCapability::NetworkIsolation
| SecurityCapability::ProcessIsolation
| SecurityCapability::ProcessTreeContainment
| SecurityCapability::ResourceLimits
| SecurityCapability::HostEvidence
)
}
#[cfg(not(target_os = "windows"))]
{
let _ = capability;
false
}
}
fn prepare(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
) -> EnforcementReport {
#[cfg(target_os = "windows")]
{
let facts = super::windows_enforce::ProbeFacts::current(policy.net_mode == "off");
self.prepare_with_facts(policy, identity, &facts)
}
#[cfg(not(target_os = "windows"))]
{
let states: BTreeMap<SecurityCapability, EnforcementState> = SecurityCapability::all()
.into_iter()
.map(|c| (c, EnforcementState::Unsupported))
.collect();
let report = EnforcementReport::build(
BackendKind::Windows,
policy,
identity,
&states,
&BTreeMap::new(),
true,
);
self.report = Some(report.clone());
report
}
}
fn enforcement(&self) -> Option<&EnforcementReport> {
self.report.as_ref()
}
fn note_spawned(&mut self, pid: u32) {
self.spawned_pid = Some(pid);
}
fn note_host_verified(&mut self, verification: &HostVerification) {
let Some(report) = self.report.as_ref() else {
return;
};
if !report.preparation_ok {
return;
}
if verification.win_in_job {
self.promote_configured(SecurityCapability::FilesystemIsolation);
self.promote_configured(SecurityCapability::NetworkIsolation);
self.promote_configured(SecurityCapability::ProcessIsolation);
if verification.win_kill_on_close {
self.promote_configured(SecurityCapability::ProcessTreeContainment);
}
}
if verification.win_job_ceiling {
self.promote_configured(SecurityCapability::ResourceLimits);
}
if verification.win_low_integrity {
if let Some(report) = self.report.as_mut() {
if let Some(record) = report.records.iter_mut().find(|r| {
r.capability == SecurityCapability::ProcessIsolation
&& r.state == EnforcementState::Enforced
}) {
record.state = EnforcementState::Verified;
}
}
}
}
fn note_failed(&mut self, kind: PreparationFailureKind) {
if let Some(report) = self.report.as_mut() {
for record in &mut report.records {
match record.state {
EnforcementState::Configured
| EnforcementState::Enforced
| EnforcementState::Verified => {
record.state = EnforcementState::Failed;
record.failure = Some(kind);
}
_ => {}
}
}
report.preparation_ok = false;
}
}
fn note_tree_clean(&mut self, clean: bool) {
let Some(report) = self.report.as_mut() else {
return;
};
if let Some(record) = report
.records
.iter_mut()
.find(|r| r.capability == SecurityCapability::ProcessTreeContainment)
{
match record.state {
EnforcementState::Enforced if clean => {
record.state = EnforcementState::Verified;
}
EnforcementState::Enforced | EnforcementState::Configured if !clean => {
record.state = EnforcementState::Failed;
record.failure = Some(PreparationFailureKind::VerificationUnavailable);
}
_ => {}
}
}
}
fn note_diagnostic(&mut self, diag: String) {
let diag = match &self.diagnostic {
Some(st) => format!("{st} {diag}"),
None => diag,
};
self.diagnostic = Some(diag);
}
fn diagnostic(&self) -> Option<String> {
self.diagnostic.clone()
}
fn teardown(&mut self) {
self.report = None;
self.spawned_pid = None;
self.diagnostic = None;
}
}
pub fn select_backend(kind: BackendKind) -> Box<dyn SandboxBackend> {
match kind {
BackendKind::Direct => Box::new(DirectBackend::new()),
BackendKind::Linux => Box::new(LinuxBackend::new()),
BackendKind::Macos => Box::new(MacosBackend::new()),
BackendKind::Windows => Box::new(WindowsBackend::new()),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MatrixEntry {
pub backend: BackendKind,
pub capability: SecurityCapability,
pub supported: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformMatrix {
pub entries: Vec<MatrixEntry>,
}
impl PlatformMatrix {
pub fn current() -> Self {
let mut entries = Vec::new();
for kind in BackendKind::all() {
let backend = select_backend(kind);
for capability in SecurityCapability::all() {
entries.push(MatrixEntry {
backend: kind,
capability,
supported: backend.supports(capability),
});
}
}
PlatformMatrix { entries }
}
pub fn supports(&self, backend: BackendKind, capability: SecurityCapability) -> bool {
self.entries
.iter()
.find(|e| e.backend == backend && e.capability == capability)
.map(|e| e.supported)
.unwrap_or(false)
}
pub fn render(&self) -> String {
let mut rows: Vec<String> = self
.entries
.iter()
.map(|e| {
format!(
"{} {} {}",
e.backend.label(),
e.capability.label(),
if e.supported {
"supported"
} else {
"unsupported"
}
)
})
.collect();
rows.sort();
rows.join("\n")
}
}
pub fn required_capabilities(scenario: &Scenario) -> Vec<SecurityCapability> {
match scenario.category {
Category::Aux => vec![SecurityCapability::HostEvidence],
Category::Spawn => vec![
SecurityCapability::ProcessIsolation,
SecurityCapability::HostEvidence,
],
Category::FsRead | Category::FsWrite => vec![
SecurityCapability::FilesystemIsolation,
SecurityCapability::ExecutionRootIsolation,
SecurityCapability::HostEvidence,
],
Category::Net => vec![
SecurityCapability::NetworkIsolation,
SecurityCapability::HostEvidence,
],
Category::Proc => vec![
SecurityCapability::ProcessIsolation,
SecurityCapability::ProcessTreeContainment,
SecurityCapability::HostEvidence,
],
Category::Secrets => vec![
SecurityCapability::FilesystemIsolation,
SecurityCapability::HostEvidence,
],
}
}
pub fn allows_pass(report: &EnforcementReport, scenario: &Scenario) -> bool {
report.allows_pass(&required_capabilities(scenario))
}
pub fn apply_backend_ceiling(
verdict: Verdict,
report: &EnforcementReport,
scenario: &Scenario,
) -> Verdict {
match verdict {
Verdict::Pass if !allows_pass(report, scenario) => Verdict::Inconclusive,
other => other,
}
}
#[cfg(test)]
mod backend_arch_tests {
use super::*;
use crate::verify_ng::model::ClaimStrength;
use crate::verify_ng::registry::Severity;
use std::collections::BTreeMap;
fn test_scenario(id: &str, category: Category) -> Scenario {
Scenario {
id: id.to_string(),
category,
severity: Severity::High,
required_caps: Vec::new(),
strength: BTreeMap::from([("linux-full".to_string(), ClaimStrength::Strong)]),
quorum: 1,
known_limitation: "backend-arch unit test".to_string(),
residual_risk: String::new(),
}
}
fn test_policy_and_identity() -> (CanonicalPolicy, ExecutionIdentity) {
let spec = FrozenSpec {
scenario_id: "TEST-BACKEND-001".to_string(),
registry_hash: "reg-test".to_string(),
tier: "direct".to_string(),
net_mode: "off".to_string(),
backend: "direct-exec (no sandbox; plumbing only)".to_string(),
argv: vec!["sh".to_string()],
env: BTreeMap::new(),
cwd: PathBuf::from("/tmp"),
allow_read: Vec::new(),
allow_write: Vec::new(),
deny_read: Vec::new(),
deny_write: Vec::new(),
deny_resolved: Vec::new(),
nonce: "nonce-test".to_string(),
policy_bytes: b"test-policy".to_vec(),
};
let policy = CanonicalPolicy::from_frozen(&spec);
let identity = ExecutionIdentity::new(
"TEST-BACKEND-001",
"nonce-test",
"reg-test",
policy.frozen_hash.as_str(),
);
(policy, identity)
}
#[test]
fn test_backend_capability_001_reports_explicitly() {
for kind in BackendKind::all() {
let backend = select_backend(kind);
assert_eq!(backend.kind(), kind);
assert!(!backend.name().is_empty());
let mut seen = std::collections::BTreeSet::new();
for cap in SecurityCapability::all() {
let supported = backend.supports(cap);
let _ = supported;
assert!(seen.insert(cap), "each capability reported once");
}
assert_eq!(seen.len(), SecurityCapability::all().len());
let listed = backend.supported_capabilities();
for cap in &listed {
assert!(backend.supports(*cap));
}
assert_eq!(
listed.len(),
listed
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len()
);
}
let matrix = PlatformMatrix::current();
assert_eq!(
matrix.entries.len(),
BackendKind::all().len() * SecurityCapability::all().len()
);
assert!(matrix.render().contains("direct host-evidence supported"));
}
#[test]
fn test_backend_unsupported_001_cannot_pass() {
let scenario = test_scenario("TEST-BACKEND-UNSUPPORTED-001", Category::FsRead);
let (policy, identity) = test_policy_and_identity();
let mut backend = LinuxBackend::new();
let report = backend.prepare(&policy, &identity);
assert!(!report.is_enforced(SecurityCapability::FilesystemIsolation));
assert!(!allows_pass(&report, &scenario));
assert_eq!(
apply_backend_ceiling(Verdict::Pass, &report, &scenario),
Verdict::Inconclusive
);
assert_eq!(
apply_backend_ceiling(Verdict::Fail, &report, &scenario),
Verdict::Fail,
"ceiling never converts FAIL"
);
}
#[test]
fn test_backend_policy_001_no_platform_mutation() {
let (policy, identity) = test_policy_and_identity();
let policy_before = policy.clone();
let mut backend = LinuxBackend::new();
let report = backend.prepare(&policy, &identity);
assert_eq!(policy, policy_before, "backend must not mutate policy");
assert_eq!(report.policy_hash, policy.policy_hash);
assert_eq!(report.frozen_hash, policy.frozen_hash);
let again = CanonicalPolicy::from_frozen(&FrozenSpec {
scenario_id: policy.scenario_id.clone(),
registry_hash: policy.registry_hash.clone(),
tier: policy.tier.clone(),
net_mode: policy.net_mode.clone(),
backend: policy.backend_hint.clone(),
argv: policy.argv.clone(),
env: policy.env.clone(),
cwd: policy.cwd.clone(),
allow_read: Vec::new(),
allow_write: Vec::new(),
deny_read: Vec::new(),
deny_write: Vec::new(),
deny_resolved: Vec::new(),
nonce: policy.session_nonce.clone(),
policy_bytes: policy.policy_bytes.clone(),
});
assert_eq!(policy, again, "same frozen input is stable");
}
#[test]
fn test_backend_identity_001_binds_execution() {
let (policy, identity) = test_policy_and_identity();
let mut backend = DirectBackend::new();
let report = backend.prepare(&policy, &identity);
assert!(report.binds_identity(&identity));
let mut foreign = identity.clone();
foreign.session_nonce = "other-nonce".to_string();
assert!(!report.binds_identity(&foreign));
let mut foreign = identity.clone();
foreign.scenario_id = "OTHER".to_string();
assert!(!report.binds_identity(&foreign));
let mut foreign = identity.clone();
foreign.registry_hash = "other-reg".to_string();
assert!(!report.binds_identity(&foreign));
let mut foreign = identity.clone();
foreign.frozen_hash = "other-frozen".to_string();
assert!(!report.binds_identity(&foreign));
}
#[test]
fn test_backend_fail_closed_001_failure_never_passes() {
struct FailingBackend {
report: Option<EnforcementReport>,
}
impl SandboxBackend for FailingBackend {
fn kind(&self) -> BackendKind {
BackendKind::Linux
}
fn name(&self) -> &'static str {
"failing test double"
}
fn supports(&self, _capability: SecurityCapability) -> bool {
false
}
fn prepare(
&mut self,
policy: &CanonicalPolicy,
identity: &ExecutionIdentity,
) -> EnforcementReport {
let states: BTreeMap<SecurityCapability, EnforcementState> =
SecurityCapability::all()
.into_iter()
.map(|c| (c, EnforcementState::Failed))
.collect();
let failures: BTreeMap<SecurityCapability, PreparationFailureKind> =
SecurityCapability::all()
.into_iter()
.map(|c| (c, PreparationFailureKind::SpawnRefused))
.collect();
let report = EnforcementReport::build(
BackendKind::Linux,
policy,
identity,
&states,
&failures,
false,
);
self.report = Some(report.clone());
report
}
fn enforcement(&self) -> Option<&EnforcementReport> {
self.report.as_ref()
}
fn teardown(&mut self) {
self.report = None;
}
}
let scenario = test_scenario("TEST-BACKEND-FAIL-CLOSED-001", Category::Aux);
let (policy, identity) = test_policy_and_identity();
let mut backend = FailingBackend { report: None };
let report = backend.prepare(&policy, &identity);
assert!(!report.preparation_ok);
assert!(!report.failed().is_empty());
assert!(!allows_pass(&report, &scenario));
assert_eq!(
apply_backend_ceiling(Verdict::Pass, &report, &scenario),
Verdict::Inconclusive
);
}
#[test]
fn test_backend_no_fake_enforcement_001() {
let (policy, identity) = test_policy_and_identity();
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
for kind in [BackendKind::Macos, BackendKind::Windows] {
let mut backend = select_backend(kind);
let report = backend.prepare(&policy, &identity);
for cap in SecurityCapability::all() {
assert!(
!report.is_enforced(cap),
"{kind:?} {cap:?} must not be enforced"
);
assert_ne!(report.state(cap), EnforcementState::Enforced);
assert_ne!(report.state(cap), EnforcementState::Verified);
}
assert_eq!(
report.state(SecurityCapability::FilesystemIsolation),
EnforcementState::Unsupported
);
}
#[cfg(not(target_os = "macos"))]
{
let mut backend = select_backend(BackendKind::Macos);
let report = backend.prepare(&policy, &identity);
assert!(
report.enforced().is_empty(),
"macOS must enforce nothing off macOS"
);
for cap in SecurityCapability::all() {
assert!(!report.is_enforced(cap));
assert_ne!(report.state(cap), EnforcementState::Enforced);
assert_ne!(report.state(cap), EnforcementState::Verified);
}
assert_eq!(
report.state(SecurityCapability::FilesystemIsolation),
EnforcementState::Unsupported
);
}
#[cfg(not(target_os = "windows"))]
{
let mut backend = select_backend(BackendKind::Windows);
let report = backend.prepare(&policy, &identity);
for cap in SecurityCapability::all() {
assert!(
!report.is_enforced(cap),
"Windows {cap:?} must not be enforced off Windows"
);
assert_ne!(report.state(cap), EnforcementState::Enforced);
assert_ne!(report.state(cap), EnforcementState::Verified);
}
assert_eq!(
report.state(SecurityCapability::FilesystemIsolation),
EnforcementState::Unsupported
);
}
#[cfg(target_os = "macos")]
{
let mut backend = select_backend(BackendKind::Macos);
let report = backend.prepare(&policy, &identity);
for cap in SecurityCapability::all() {
if cap == SecurityCapability::HostEvidence {
continue;
}
assert!(!report.is_enforced(cap));
assert_ne!(report.state(cap), EnforcementState::Enforced);
assert_ne!(report.state(cap), EnforcementState::Verified);
}
}
#[cfg(target_os = "windows")]
{
let mut backend = select_backend(BackendKind::Windows);
let report = backend.prepare(&policy, &identity);
for cap in SecurityCapability::all() {
if cap == SecurityCapability::HostEvidence {
continue;
}
assert!(
!report.is_enforced(cap),
"Windows {cap:?} must not be enforced before host evidence"
);
assert_ne!(report.state(cap), EnforcementState::Enforced);
assert_ne!(report.state(cap), EnforcementState::Verified);
}
backend.note_spawned(1234);
let report = backend
.enforcement()
.expect("prepared report survives a spawn note");
for cap in SecurityCapability::all() {
if cap == SecurityCapability::HostEvidence {
continue;
}
assert!(
!report.is_enforced(cap),
"Windows {cap:?} must not enforce on spawn note alone"
);
}
}
{
let mut backend = select_backend(BackendKind::Linux);
let report = backend.prepare(&policy, &identity);
for cap in SecurityCapability::all() {
if cap == SecurityCapability::HostEvidence {
continue;
}
assert!(
!report.is_enforced(cap),
"Linux {cap:?} must not be enforced before spawn"
);
assert_ne!(report.state(cap), EnforcementState::Enforced);
assert_ne!(report.state(cap), EnforcementState::Verified);
}
#[cfg(target_os = "linux")]
{
assert!(report.preparation_ok);
}
}
let mut direct = DirectBackend::new();
let report = direct.prepare(&policy, &identity);
assert!(report.is_enforced(SecurityCapability::HostEvidence));
for cap in SecurityCapability::all() {
if cap != SecurityCapability::HostEvidence {
assert!(!report.is_enforced(cap));
}
}
}
#[test]
fn test_backend_oracle_purity_001() {
use crate::verify_ng::evidence::Evidence;
use crate::verify_ng::oracle::{judge, OracleInput};
let scenario = test_scenario("TEST-BACKEND-ORACLE-PURITY-001", Category::Aux);
let id = ExecutionIdentity::new(
"TEST-BACKEND-ORACLE-PURITY-001",
"nonce-1",
"reg-test",
"frozen-test",
);
let expected =
crate::verify_ng::evidence::derive_expected_response("test-challenge", "nonce-1");
let verified =
crate::verify_ng::evidence::attest_control(&id, &expected, expected.as_bytes())
.expect("mint");
let mut evidence = Evidence::default();
evidence.host_fact("wait-status", "exit=0".to_string());
evidence.host_control_fact(&verified);
let input = OracleInput {
scenario: &scenario,
evidence: &evidence,
nonce: Some("nonce-1"),
probe_nonce: Some("nonce-1"),
control_nonce: Some("nonce-1"),
payload_intact: true,
env_poisoned: false,
agreeing_vectors: 1,
violation_observed: false,
control_observed: true,
stdio_complete: true,
execution_identity: Some(&id),
};
let first = judge(&input);
let second = judge(&input);
assert_eq!(
first, second,
"oracle is deterministic over structured input"
);
assert_eq!(first, Verdict::Pass);
let (policy, _) = test_policy_and_identity();
let policy_identity = ExecutionIdentity::new(
"TEST-BACKEND-ORACLE-PURITY-001",
"nonce-1",
"reg-test",
"frozen-test",
);
let mut backend = DirectBackend::new();
let report = backend.prepare(&policy, &policy_identity);
let gated = apply_backend_ceiling(first, &report, &scenario);
assert_eq!(apply_backend_ceiling(first, &report, &scenario), gated);
}
}