use serde::Serialize;
use crate::orchestration::{CapabilityPolicy, UnixSocketEnforcement};
use crate::stdlib::sandbox::unix_socket_enforcement;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConformanceCase {
WorkspaceWriteAdmitted,
OutsideWriteRefused,
OutsideReadRefused,
GuardianOutsideWriteRefused,
UndeclaredEnvironmentNameWithheld,
GuardianUndeclaredEnvironmentNameWithheld,
UnixSocketBindUnderRoot,
UnixSocketBindUnderRootWithNetwork,
UnixSocketBindOutsideRootRefused,
}
impl ConformanceCase {
pub const ALL: &'static [ConformanceCase] = &[
Self::WorkspaceWriteAdmitted,
Self::OutsideWriteRefused,
Self::OutsideReadRefused,
Self::GuardianOutsideWriteRefused,
Self::UndeclaredEnvironmentNameWithheld,
Self::GuardianUndeclaredEnvironmentNameWithheld,
Self::UnixSocketBindUnderRoot,
Self::UnixSocketBindUnderRootWithNetwork,
Self::UnixSocketBindOutsideRootRefused,
];
pub fn id(self) -> &'static str {
match self {
Self::WorkspaceWriteAdmitted => "fs.workspace_write_admitted",
Self::OutsideWriteRefused => "fs.outside_write_refused",
Self::OutsideReadRefused => "fs.outside_read_refused",
Self::GuardianOutsideWriteRefused => "guardian.outside_write_refused",
Self::UndeclaredEnvironmentNameWithheld => "env.undeclared_name_withheld",
Self::GuardianUndeclaredEnvironmentNameWithheld => {
"guardian.undeclared_env_name_withheld"
}
Self::UnixSocketBindUnderRoot => "unix_socket.bind_under_root",
Self::UnixSocketBindUnderRootWithNetwork => "unix_socket.bind_under_root_with_network",
Self::UnixSocketBindOutsideRootRefused => "unix_socket.bind_outside_root_refused",
}
}
pub fn route(self) -> SpawnRoute {
match self {
Self::GuardianOutsideWriteRefused | Self::GuardianUndeclaredEnvironmentNameWithheld => {
SpawnRoute::Guardian
}
_ => SpawnRoute::Direct,
}
}
pub fn needs_filesystem_enforcement(self) -> bool {
!matches!(
self,
Self::UndeclaredEnvironmentNameWithheld
| Self::GuardianUndeclaredEnvironmentNameWithheld
)
}
pub fn expectation(self, policy: &CapabilityPolicy) -> Expectation {
if self.route() == SpawnRoute::Guardian && !cfg!(unix) {
return Expectation::NotApplicable(
"no process-owner guardian on this platform: a process tree is contained \
with a Job Object and the command is never re-created",
);
}
match self {
Self::WorkspaceWriteAdmitted => Expectation::Observe(Observation::Admitted),
Self::OutsideWriteRefused
| Self::OutsideReadRefused
| Self::GuardianOutsideWriteRefused
| Self::UndeclaredEnvironmentNameWithheld
| Self::GuardianUndeclaredEnvironmentNameWithheld => {
Expectation::Observe(Observation::Refused)
}
Self::UnixSocketBindUnderRoot | Self::UnixSocketBindUnderRootWithNetwork => {
match unix_socket_enforcement(policy) {
UnixSocketEnforcement::Refused => {
Expectation::Observe(Observation::SpawnRefused)
}
UnixSocketEnforcement::NotRequested => {
Expectation::Observe(Observation::Refused)
}
UnixSocketEnforcement::PathScoped
| UnixSocketEnforcement::ServeOnly
| UnixSocketEnforcement::SupersededByNetworkGrant => {
Expectation::Observe(Observation::Admitted)
}
}
}
Self::UnixSocketBindOutsideRootRefused => match unix_socket_enforcement(policy) {
UnixSocketEnforcement::Refused => Expectation::Observe(Observation::SpawnRefused),
_ => Expectation::Observe(Observation::Refused),
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SpawnRoute {
Direct,
Guardian,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Observation {
Admitted,
Refused,
SpawnRefused,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Expectation {
Observe(Observation),
NotApplicable(&'static str),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(tag = "verdict", rename_all = "snake_case")]
pub enum Verdict {
Conforms,
Escaped {
target: String,
},
Overrefused {
target: String,
observed: Observation,
},
NotMeasured {
reason: String,
},
NotApplicable {
reason: String,
},
Contradiction {
target: String,
reason: String,
},
ProbeBroken {
reason: String,
},
}
impl Verdict {
pub fn is_failure(&self) -> bool {
matches!(
self,
Self::Escaped { .. }
| Self::Overrefused { .. }
| Self::Contradiction { .. }
| Self::ProbeBroken { .. }
)
}
}
pub fn judge(
case: ConformanceCase,
expectation: Expectation,
enforcing: bool,
observed: Observation,
target: &str,
) -> Verdict {
let expected = match expectation {
Expectation::NotApplicable(reason) => {
return Verdict::NotApplicable {
reason: reason.to_string(),
}
}
Expectation::Observe(expected) => expected,
};
if !enforcing && case.needs_filesystem_enforcement() {
return match observed {
Observation::Admitted => Verdict::NotMeasured {
reason: "the active backend reports no filesystem enforcement on this host, \
and the operation was observed to take effect"
.to_string(),
},
Observation::Refused | Observation::SpawnRefused => Verdict::Contradiction {
target: target.to_string(),
reason: format!(
"the active backend reports no filesystem enforcement, yet the operation \
was {observed:?} where {expected:?} was required"
),
},
};
}
if observed == expected {
return Verdict::Conforms;
}
match (expected, observed) {
(Observation::Refused | Observation::SpawnRefused, Observation::Admitted) => {
Verdict::Escaped {
target: target.to_string(),
}
}
_ => Verdict::Overrefused {
target: target.to_string(),
observed,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
const REFUSE: Expectation = Expectation::Observe(Observation::Refused);
const ADMIT: Expectation = Expectation::Observe(Observation::Admitted);
#[test]
fn an_admitted_operation_under_a_refusal_case_escapes_and_names_the_target() {
let verdict = judge(
ConformanceCase::OutsideWriteRefused,
REFUSE,
true,
Observation::Admitted,
"/outside/probe",
);
assert_eq!(
verdict,
Verdict::Escaped {
target: "/outside/probe".to_string()
}
);
assert!(verdict.is_failure());
}
#[test]
fn a_refused_admission_case_fails_as_overrefusal() {
let verdict = judge(
ConformanceCase::WorkspaceWriteAdmitted,
ADMIT,
true,
Observation::Refused,
"/ws/probe",
);
assert!(verdict.is_failure(), "{verdict:?}");
}
#[test]
fn an_unenforcing_host_is_not_measured_and_never_conforms() {
let verdict = judge(
ConformanceCase::OutsideWriteRefused,
REFUSE,
false,
Observation::Admitted,
"/outside/probe",
);
assert!(
matches!(verdict, Verdict::NotMeasured { .. }),
"{verdict:?}"
);
assert!(!verdict.is_failure());
}
#[test]
fn an_unenforcing_host_that_refuses_is_a_contradiction() {
let verdict = judge(
ConformanceCase::OutsideWriteRefused,
REFUSE,
false,
Observation::Refused,
"/outside/probe",
);
assert!(
matches!(verdict, Verdict::Contradiction { .. }),
"{verdict:?}"
);
}
#[test]
fn environment_cases_are_measured_without_filesystem_enforcement() {
let verdict = judge(
ConformanceCase::UndeclaredEnvironmentNameWithheld,
REFUSE,
false,
Observation::Admitted,
"UNDECLARED_PROBE",
);
assert!(matches!(verdict, Verdict::Escaped { .. }), "{verdict:?}");
}
#[test]
fn case_ids_are_unique() {
let mut ids: Vec<_> = ConformanceCase::ALL.iter().map(|case| case.id()).collect();
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), ConformanceCase::ALL.len());
}
}