use serde::{Deserialize, Serialize};
use crate::backend::{Backend, SandboxBackend};
use crate::lifecycle::{ExecResult, SandboxConfig};
use crate::policy::SandboxPolicy;
const DEFAULT_IMAGE: &str = "ghcr.io/maccracken/sy-agnos:latest";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SyAgnosTier {
Minimal,
DmVerity,
TpmMeasured,
}
impl SyAgnosTier {
pub fn strength(&self) -> u8 {
match self {
Self::Minimal => 80,
Self::DmVerity => 85,
Self::TpmMeasured => 88,
}
}
pub fn parse(s: &str) -> Self {
match s.trim() {
"tpm_measured" => Self::TpmMeasured,
"dmverity" => Self::DmVerity,
_ => Self::Minimal,
}
}
}
impl std::fmt::Display for SyAgnosTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Minimal => write!(f, "minimal"),
Self::DmVerity => write!(f, "dmverity"),
Self::TpmMeasured => write!(f, "tpm_measured"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationReport {
pub pcr_values: std::collections::HashMap<u32, String>,
pub hmac_signature: Option<String>,
pub algorithm: Option<String>,
pub timestamp: Option<String>,
}
impl AttestationReport {
pub fn verify(&self) -> bool {
let required_pcrs = [8, 9, 10];
let pcr_pattern = regex_lite::Regex::new(r"^[0-9a-f]{16,128}$").ok();
for pcr in &required_pcrs {
match self.pcr_values.get(pcr) {
Some(val) => {
if let Some(ref re) = pcr_pattern
&& !re.is_match(&val.to_lowercase())
{
return false;
}
}
None => return false,
}
}
if let Some(ref sig) = self.hmac_signature {
if sig.len() < 32 {
return false;
}
} else {
return false;
}
true
}
}
#[derive(Debug)]
pub struct SyAgnosBackend {
config: SandboxConfig,
runtime: String,
image: String,
}
impl SyAgnosBackend {
pub fn new(config: &SandboxConfig) -> crate::Result<Self> {
let runtime = detect_runtime().ok_or_else(|| {
crate::KavachError::BackendUnavailable(
"no container runtime (docker/podman) found".into(),
)
})?;
Ok(Self {
config: config.clone(),
runtime,
image: DEFAULT_IMAGE.into(),
})
}
pub async fn detect_tier(&self) -> SyAgnosTier {
let output = tokio::process::Command::new(&self.runtime)
.args([
"run",
"--rm",
"--entrypoint",
"cat",
&self.image,
"/etc/sy-agnos-release",
])
.output()
.await;
match output {
Ok(out) if out.status.success() => {
let content = String::from_utf8_lossy(&out.stdout);
parse_tier_from_release(&content)
}
_ => SyAgnosTier::Minimal,
}
}
}
#[async_trait::async_trait]
impl SandboxBackend for SyAgnosBackend {
fn backend_type(&self) -> Backend {
Backend::SyAgnos
}
async fn exec(&self, command: &str, policy: &SandboxPolicy) -> crate::Result<ExecResult> {
let _ = policy;
let mut args = vec![
"run".to_string(),
"--rm".to_string(),
"--network".to_string(),
if self.config.policy.network.enabled {
"bridge"
} else {
"none"
}
.to_string(),
];
if let Some(mb) = self.config.policy.memory_limit_mb {
args.extend(["--memory".into(), format!("{mb}m")]);
}
if let Some(cpu) = self.config.policy.cpu_limit {
args.extend(["--cpus".into(), format!("{cpu}")]);
}
if let Some(pids) = self.config.policy.max_pids {
args.extend(["--pids-limit".into(), pids.to_string()]);
}
if self.config.policy.read_only_rootfs {
args.push("--read-only".into());
}
for (k, v) in &self.config.env {
args.extend(["-e".into(), format!("{k}={v}")]);
}
args.extend([
"--entrypoint".into(),
"/bin/sh".into(),
self.image.clone(),
"-c".into(),
command.into(),
]);
let mut cmd = tokio::process::Command::new(&self.runtime);
cmd.args(&args);
crate::backend::exec_util::execute_with_timeout(
&mut cmd,
self.config.timeout_ms,
&self.runtime,
)
.await
}
async fn health_check(&self) -> crate::Result<bool> {
let output = tokio::process::Command::new(&self.runtime)
.args(["image", "inspect", &self.image])
.output()
.await
.map_err(|e| crate::KavachError::ExecFailed(format!("sy-agnos health: {e}")))?;
Ok(output.status.success())
}
async fn destroy(&self) -> crate::Result<()> {
Ok(())
}
}
fn parse_tier_from_release(content: &str) -> SyAgnosTier {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(content)
&& let Some(tier) = json.get("tier").and_then(|t| t.as_str())
{
return SyAgnosTier::parse(tier);
}
for line in content.lines() {
if let Some(val) = line.strip_prefix("tier=") {
return SyAgnosTier::parse(val);
}
}
SyAgnosTier::Minimal
}
fn detect_runtime() -> Option<String> {
crate::backend::which_first(&["docker", "podman"]).map(Into::into)
}
mod regex_lite {
pub struct Regex(String);
impl Regex {
pub fn new(pattern: &str) -> Result<Self, ()> {
Ok(Self(pattern.to_string()))
}
pub fn is_match(&self, text: &str) -> bool {
if self.0 == r"^[0-9a-f]{16,128}$" {
text.len() >= 16 && text.len() <= 128 && text.chars().all(|c| c.is_ascii_hexdigit())
} else {
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tier_from_str() {
assert_eq!(SyAgnosTier::parse("minimal"), SyAgnosTier::Minimal);
assert_eq!(SyAgnosTier::parse("dmverity"), SyAgnosTier::DmVerity);
assert_eq!(SyAgnosTier::parse("tpm_measured"), SyAgnosTier::TpmMeasured);
assert_eq!(SyAgnosTier::parse("unknown"), SyAgnosTier::Minimal);
}
#[test]
fn tier_strength() {
assert_eq!(SyAgnosTier::Minimal.strength(), 80);
assert_eq!(SyAgnosTier::DmVerity.strength(), 85);
assert_eq!(SyAgnosTier::TpmMeasured.strength(), 88);
}
#[test]
fn tier_display() {
assert_eq!(SyAgnosTier::Minimal.to_string(), "minimal");
assert_eq!(SyAgnosTier::DmVerity.to_string(), "dmverity");
assert_eq!(SyAgnosTier::TpmMeasured.to_string(), "tpm_measured");
}
#[test]
fn parse_tier_json() {
let content = r#"{"tier": "dmverity", "version": "2026.3.21"}"#;
assert_eq!(parse_tier_from_release(content), SyAgnosTier::DmVerity);
}
#[test]
fn parse_tier_keyvalue() {
let content = "version=2026.3.21\ntier=tpm_measured\nstrength=88\n";
assert_eq!(parse_tier_from_release(content), SyAgnosTier::TpmMeasured);
}
#[test]
fn parse_tier_unknown_defaults_minimal() {
assert_eq!(parse_tier_from_release(""), SyAgnosTier::Minimal);
assert_eq!(parse_tier_from_release("garbage"), SyAgnosTier::Minimal);
}
#[test]
fn attestation_verify_valid() {
let mut pcrs = std::collections::HashMap::new();
pcrs.insert(8, "abcdef0123456789abcdef0123456789".to_string());
pcrs.insert(9, "1234567890abcdef1234567890abcdef".to_string());
pcrs.insert(10, "fedcba9876543210fedcba9876543210".to_string());
let report = AttestationReport {
pcr_values: pcrs,
hmac_signature: Some("a".repeat(64)),
algorithm: Some("SHA-256".into()),
timestamp: Some("2026-03-21T00:00:00Z".into()),
};
assert!(report.verify());
}
#[test]
fn attestation_verify_missing_pcr() {
let mut pcrs = std::collections::HashMap::new();
pcrs.insert(8, "abcdef0123456789abcdef0123456789".to_string());
let report = AttestationReport {
pcr_values: pcrs,
hmac_signature: Some("a".repeat(64)),
algorithm: None,
timestamp: None,
};
assert!(!report.verify());
}
#[test]
fn attestation_verify_short_hmac() {
let mut pcrs = std::collections::HashMap::new();
pcrs.insert(8, "abcdef0123456789abcdef0123456789".to_string());
pcrs.insert(9, "1234567890abcdef1234567890abcdef".to_string());
pcrs.insert(10, "fedcba9876543210fedcba9876543210".to_string());
let report = AttestationReport {
pcr_values: pcrs,
hmac_signature: Some("short".into()),
algorithm: None,
timestamp: None,
};
assert!(!report.verify());
}
#[test]
fn attestation_verify_no_hmac() {
let mut pcrs = std::collections::HashMap::new();
pcrs.insert(8, "abcdef0123456789abcdef0123456789".to_string());
pcrs.insert(9, "1234567890abcdef1234567890abcdef".to_string());
pcrs.insert(10, "fedcba9876543210fedcba9876543210".to_string());
let report = AttestationReport {
pcr_values: pcrs,
hmac_signature: None,
algorithm: None,
timestamp: None,
};
assert!(!report.verify());
}
#[test]
fn detect_runtime_does_not_panic() {
let _ = detect_runtime();
}
}