use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::model::{Category, ClaimStrength};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Target {
LinuxFull,
LinuxFsOnly,
LinuxSeccomp,
Macos,
Windows,
WindowsSandboxVm,
}
impl Target {
pub fn label(self) -> &'static str {
match self {
Target::LinuxFull => "linux-full",
Target::LinuxFsOnly => "linux-fsonly",
Target::LinuxSeccomp => "linux-seccomp",
Target::Macos => "macos",
Target::Windows => "windows",
Target::WindowsSandboxVm => "windows-sandbox-vm",
}
}
}
impl Severity {
pub fn label(self) -> &'static str {
match self {
Severity::Blocker => "blocker",
Severity::High => "high",
Severity::Medium => "medium",
Severity::Low => "low",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Blocker,
High,
Medium,
Low,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Scenario {
pub id: String,
pub category: Category,
pub severity: Severity,
pub required_caps: Vec<String>,
pub strength: BTreeMap<String, ClaimStrength>,
pub quorum: usize,
pub known_limitation: String,
#[serde(default)]
pub residual_risk: String,
}
impl Scenario {
pub fn strength_for(&self, target: Target) -> ClaimStrength {
self.strength
.get(target.label())
.copied()
.unwrap_or(ClaimStrength::Unsupported)
}
pub fn lint(&self) -> Result<(), String> {
if self.id.trim().is_empty() {
return Err("scenario id is empty".to_string());
}
if self.quorum < 1 {
return Err(format!("{}: quorum must be >= 1", self.id));
}
if self.known_limitation.trim().is_empty() {
return Err(format!("{}: known_limitation must be non-empty", self.id));
}
if self.strength.values().any(|s| *s == ClaimStrength::Partial)
&& self.residual_risk.trim().is_empty()
{
return Err(format!(
"{}: PARTIAL target requires residual_risk",
self.id
));
}
Ok(())
}
}
pub fn registry() -> Vec<Scenario> {
vec![
Scenario {
id: "ORACLE-DECEIT-001".to_string(),
category: Category::Aux,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([(Target::LinuxFull.label().to_string(), ClaimStrength::Strong)]),
quorum: 1,
known_limitation: "Proves oracle soundness only; says nothing about any enforcement backend."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "CONTROL-SPLIT-001".to_string(),
category: Category::Aux,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([(Target::LinuxFull.label().to_string(), ClaimStrength::Strong)]),
quorum: 1,
known_limitation: "Proves nonce binding of the control pair only."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "FIXTURE-MUTATE-001".to_string(),
category: Category::Aux,
severity: Severity::High,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([(Target::LinuxFull.label().to_string(), ClaimStrength::Strong)]),
quorum: 1,
known_limitation: "Proves payload integrity checking only."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "ENV-POISON-001".to_string(),
category: Category::Spawn,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::Macos.label().to_string(), ClaimStrength::Strong),
]),
quorum: 1,
known_limitation: "Proves diagnostic-env detection only; not a substitute for env scrub verification."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "GATE-VACUUM-001".to_string(),
category: Category::Aux,
severity: Severity::High,
required_caps: vec![],
strength: BTreeMap::from([(Target::LinuxFull.label().to_string(), ClaimStrength::Strong)]),
quorum: 1,
known_limitation: "Meta-test of the gate quotas; proves the gate cannot pass on an empty suite."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "HANG-GRANDCHILD-001".to_string(),
category: Category::Proc,
severity: Severity::High,
required_caps: vec!["spawn".to_string(), "tree-sweep".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::Windows.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Partial),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
]),
quorum: 1,
known_limitation: "Proves deadline-aware collection, not general liveness of arbitrary payloads."
.to_string(),
residual_risk: "On fs-only/macOS the grandchild may outlive the group kill until the sweep budget expires; verdict is FAIL/INCONCLUSIVE, never PASS.".to_string(),
},
Scenario {
id: "VFS-TRAV-001".to_string(),
category: Category::FsRead,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string(), "landlock".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Strong),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
(Target::Windows.label().to_string(), ClaimStrength::Partial),
]),
quorum: 2,
known_limitation: "Covers symlink/hardlink/dotdot/rename vectors present in the payload set; new kernel path aliases need new vectors."
.to_string(),
residual_risk: "macOS Shape-A broad reads and Windows ACL fallback may expose entries outside the tail-deny list; strength ceiling PARTIAL there.".to_string(),
},
Scenario {
id: "VFS-WRITE-001".to_string(),
category: Category::FsWrite,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string(), "landlock".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Strong),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
(Target::Windows.label().to_string(), ClaimStrength::Partial),
]),
quorum: 2,
known_limitation: "Proves writes outside allowlist are denied for the covered vectors only; writes inside $PROJECT are allowed by design and out of scope (see threat-model non-goal #2)."
.to_string(),
residual_risk: "macOS Shape-A broad paths and Windows ACL fallback may permit writes through aliases outside the deny list; strength ceiling PARTIAL there.".to_string(),
},
Scenario {
id: "NET-DNS-IPV6-001".to_string(),
category: Category::Net,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string(), "netns".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Partial),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
(Target::Windows.label().to_string(), ClaimStrength::Partial),
]),
quorum: 2,
known_limitation: "Proves --net=off isolation only; allowlist relay modes need a separate broker suite."
.to_string(),
residual_risk: "Without a network namespace (fs-only/mac/win) only syscall/capability denial is proven, not absence of a route.".to_string(),
},
Scenario {
id: "NET-EXFIL-001".to_string(),
category: Category::Net,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string(), "netns".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Partial),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
(Target::Windows.label().to_string(), ClaimStrength::Partial),
]),
quorum: 3,
known_limitation: "Proves --net=off isolation only across the listed egress vectors; allowlist relay modes and TLS-through-allowed-API payload semantics need a separate broker suite (threat-model non-goal #1)."
.to_string(),
residual_risk: "Without netns (fs-only/mac/win) only syscall/capability denial is proven, not absence of a route. Per-domain egress without admin on Windows is UNPROVABLE.".to_string(),
},
Scenario {
id: "PROC-ESC-001".to_string(),
category: Category::Proc,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string(), "tree-sweep".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::Windows.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Partial),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
]),
quorum: 1,
known_limitation: "Post-mortem sweep is host-side observation of absence; a hostile scheduler can delay reparenting past the sweep budget (INCONCLUSIVE, never PASS)."
.to_string(),
residual_risk: "fs-only setsid orphans and macOS watchdog races are known residuals; they FAIL/advisory, never PASS.".to_string(),
},
Scenario {
id: "PROC-TREE-001".to_string(),
category: Category::Proc,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string(), "tree-sweep".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::Windows.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Partial),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
]),
quorum: 2,
known_limitation: "Proves process-tree containment for the covered escape shapes only; hostile scheduler delaying reparent past the sweep budget yields INCONCLUSIVE, never PASS."
.to_string(),
residual_risk: "fs-only setsid orphans and macOS watchdog races are known residuals; verdict is FAIL/INCONCLUSIVE, never PASS.".to_string(),
},
Scenario {
id: "ENV-LEAK-001".to_string(),
category: Category::Secrets,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::Macos.label().to_string(), ClaimStrength::Strong),
(Target::Windows.label().to_string(), ClaimStrength::Strong),
]),
quorum: 1,
known_limitation: "Proves environment isolation and credential scrub under sealed SecurityContract: scrubbing of arbitrary host vars, sensitive credentials, PATH hygiene, internal Vetto vars, explicitly denied variables, and post-start mutation immutability; unknown exfil channels (covert timing, allowed-relay payloads) are out of scope."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "RACE-BINDING-001".to_string(),
category: Category::Spawn,
severity: Severity::Blocker,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([(Target::LinuxFull.label().to_string(), ClaimStrength::Strong)]),
quorum: 1,
known_limitation: "Proves spec/tier binding continuity in-process; a compromised host kernel is out of scope."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "CLEANUP-SIGKILL-001".to_string(),
category: Category::Proc,
severity: Severity::High,
required_caps: vec!["spawn".to_string(), "tree-sweep".to_string()],
strength: BTreeMap::from([
(Target::LinuxFull.label().to_string(), ClaimStrength::Strong),
(Target::Windows.label().to_string(), ClaimStrength::Strong),
(Target::LinuxFsOnly.label().to_string(), ClaimStrength::Partial),
(Target::Macos.label().to_string(), ClaimStrength::Partial),
]),
quorum: 1,
known_limitation: "Requires an external observer process/VM; cannot be self-proven from inside the harness under test."
.to_string(),
residual_risk: "fs-only/macOS orphans may survive a SIGKILLed harness; suite must run in a disposable VM there.".to_string(),
},
Scenario {
id: "EVIDENCE-REDACT-001".to_string(),
category: Category::Aux,
severity: Severity::High,
required_caps: vec![],
strength: BTreeMap::from([(Target::LinuxFull.label().to_string(), ClaimStrength::Strong)]),
quorum: 1,
known_limitation: "Proves redaction of the known secret shapes; novel shapes need secretscan updates."
.to_string(),
residual_risk: String::new(),
},
Scenario {
id: "MAC-SHAPE-001".to_string(),
category: Category::FsRead,
severity: Severity::High,
required_caps: vec!["spawn".to_string(), "seatbelt".to_string()],
strength: BTreeMap::from([(Target::Macos.label().to_string(), ClaimStrength::Partial)]),
quorum: 1,
known_limitation: "Proves the enforced profile is Shape-A + tail-deny byte-for-byte; read secrecy beyond tail-deny is UNPROVABLE on macOS native."
.to_string(),
residual_risk: "Any path outside the tail-deny list is readable by construction; use a Linux VM for strong read secrecy.".to_string(),
},
Scenario {
id: "WIN-UNC-001".to_string(),
category: Category::FsRead,
severity: Severity::High,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([(Target::Windows.label().to_string(), ClaimStrength::Partial)]),
quorum: 1,
known_limitation: "Advisory until UNC/namespace-alias coverage is mapped; starts INCONCLUSIVE, never PASS on first implementation."
.to_string(),
residual_risk: r"Alternate path aliases (UNC, \\?\, mapped drives) may bypass ACL-shaped checks.".to_string(),
},
Scenario {
id: "WIN-WSL-001".to_string(),
category: Category::FsRead,
severity: Severity::High,
required_caps: vec!["spawn".to_string()],
strength: BTreeMap::from([(Target::Windows.label().to_string(), ClaimStrength::Unsupported)]),
quorum: 1,
known_limitation: "WSL-interop boundary is unmapped; INCONCLUSIVE baseline until researched. Any PASS is an oracle bug."
.to_string(),
residual_risk: String::new(),
},
]
}
pub fn canonical_registry_bytes(scenarios: &[Scenario]) -> Vec<u8> {
let mut sorted: Vec<&Scenario> = scenarios.iter().collect();
sorted.sort_by(|a, b| a.id.cmp(&b.id));
let mut out = String::from("vng-registry-v1;");
for s in sorted {
let mut caps = s.required_caps.clone();
caps.sort();
let mut strength: Vec<(&String, &super::model::ClaimStrength)> =
s.strength.iter().collect();
strength.sort_by(|a, b| a.0.cmp(b.0));
let strength_s = strength
.iter()
.map(|(k, v)| format!("{k}={}", v.label()))
.collect::<Vec<_>>()
.join(",");
out.push_str(&format!(
"scenario[id={}|cat={}|sev={}|caps=[{}]|strength=[{}]|quorum={}|limit={}|residual={}];",
s.id,
s.category.label(),
s.severity.label(),
caps.join(","),
strength_s,
s.quorum,
s.known_limitation,
s.residual_risk
));
}
out.into_bytes()
}
pub fn registry_hash_full(scenarios: &[Scenario]) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(canonical_registry_bytes(scenarios));
super::frozen::hex_encode(&hasher.finalize())
}
pub fn lint_all(scenarios: &[Scenario]) -> Vec<String> {
let mut errors = Vec::new();
let mut seen = std::collections::BTreeSet::new();
for s in scenarios {
if !seen.insert(s.id.clone()) {
errors.push(format!("duplicate scenario id {}", s.id));
}
if let Err(e) = s.lint() {
errors.push(e);
}
}
errors
}
#[cfg(test)]
mod registry_tests {
use super::*;
#[test]
fn registry_lints_clean() {
let reg = registry();
assert!(reg.len() >= 10);
assert_eq!(lint_all(®), Vec::<String>::new());
}
#[test]
fn empty_limitation_is_rejected() {
let mut s = registry().remove(0);
s.known_limitation.clear();
assert!(s.lint().is_err());
}
#[test]
fn partial_without_residual_is_rejected() {
let mut s = registry().remove(0);
s.strength = BTreeMap::from([("linux-full".to_string(), ClaimStrength::Partial)]);
s.residual_risk.clear();
assert!(s.lint().is_err());
}
}