use serde::{Deserialize, Serialize};
use crate::signing::SignedPayload;
pub const PROTOCOL_VERSION_AUTHENTICATED: u8 = 2;
pub const PROTOCOL_VERSION_LEGACY: u8 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthenticatedFrame {
pub version: u8,
pub session_id: String,
pub sequence: u64,
pub timestamp: String,
pub signed: SignedPayload,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionHello {
pub version: u8,
pub session_id: String,
pub challenge: Vec<u8>,
pub host_pubkey: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionHelloAck {
pub version: u8,
pub session_id: String,
pub challenge_response: Vec<u8>,
pub guest_pubkey: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityPolicy {
#[serde(default = "default_true")]
pub require_auth: bool,
#[serde(default)]
pub access: AccessPolicy,
#[serde(default)]
pub rate_limits: RateLimitPolicy,
#[serde(default)]
pub session: SessionPolicy,
#[serde(default)]
pub blocklist: Vec<BlocklistEntry>,
}
impl Default for SecurityPolicy {
fn default() -> Self {
Self {
require_auth: true,
access: AccessPolicy::default(),
rate_limits: RateLimitPolicy::default(),
session: SessionPolicy::default(),
blocklist: Vec::new(),
}
}
}
impl SecurityPolicy {
pub fn dev_defaults() -> Self {
Self {
require_auth: false,
access: AccessPolicy {
console: true,
debug_exec: true,
..AccessPolicy::default()
},
..Self::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessPolicy {
#[serde(default = "default_true")]
pub filesystem: bool,
#[serde(default = "default_true")]
pub network: bool,
#[serde(default = "default_true")]
pub build: bool,
#[serde(default = "default_true")]
pub host_communication: bool,
#[serde(default)]
pub debug_exec: bool,
#[serde(default)]
pub console: bool,
}
impl Default for AccessPolicy {
fn default() -> Self {
Self {
filesystem: true,
network: true,
build: true,
host_communication: true,
debug_exec: false,
console: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitPolicy {
#[serde(default = "default_rate_fps")]
pub frames_per_second: u32,
#[serde(default = "default_rate_fpm")]
pub frames_per_minute: u32,
}
impl Default for RateLimitPolicy {
fn default() -> Self {
Self {
frames_per_second: 100,
frames_per_minute: 3000,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionPolicy {
#[serde(default)]
pub max_lifetime_secs: u64,
#[serde(default)]
pub max_tasks: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GateDecision {
Allow,
Blocked {
pattern: String,
reason: String,
},
RequiresApproval {
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ApprovalVerdict {
Approved,
Denied { reason: String },
Timeout,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BlocklistAction {
Block,
RequireApproval,
Log,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum BlocklistSeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlocklistEntry {
pub pattern: String,
pub category: String,
pub severity: BlocklistSeverity,
pub action: BlocklistAction,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ThreatCategory {
SecretExposure,
DataExfiltration,
Injection,
Destructive,
PrivilegeEscalation,
SupplyChain,
SensitiveFileAccess,
SystemModification,
NetworkAbuse,
ToolPoisoning,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Severity {
Info,
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThreatFinding {
pub category: ThreatCategory,
pub pattern_id: String,
pub severity: Severity,
pub matched_text: String,
pub context: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SecurityLayer {
JailerIsolation,
CgroupLimits,
SeccompFilter,
NetworkIsolation,
VsockAuth,
EncryptionAtRest,
EncryptionInTransit,
AuditLogging,
SecretManagement,
ConfigImmutability,
GuestHardening,
SupplyChainIntegrity,
}
impl SecurityLayer {
pub fn all() -> &'static [SecurityLayer] {
&[
SecurityLayer::JailerIsolation,
SecurityLayer::CgroupLimits,
SecurityLayer::SeccompFilter,
SecurityLayer::NetworkIsolation,
SecurityLayer::VsockAuth,
SecurityLayer::EncryptionAtRest,
SecurityLayer::EncryptionInTransit,
SecurityLayer::AuditLogging,
SecurityLayer::SecretManagement,
SecurityLayer::ConfigImmutability,
SecurityLayer::GuestHardening,
SecurityLayer::SupplyChainIntegrity,
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostureCheck {
pub layer: SecurityLayer,
pub name: String,
pub passed: bool,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostureReport {
pub checks: Vec<PostureCheck>,
pub score: f64,
pub timestamp: String,
}
fn default_true() -> bool {
true
}
fn default_rate_fps() -> u32 {
100
}
fn default_rate_fpm() -> u32 {
3000
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_authenticated_frame_serde_roundtrip() {
let frame = AuthenticatedFrame {
version: PROTOCOL_VERSION_AUTHENTICATED,
session_id: "sess-001".to_string(),
sequence: 42,
timestamp: "2026-02-25T00:00:00Z".to_string(),
signed: SignedPayload {
payload: b"inner request json".to_vec(),
signature: vec![0u8; 64],
signer_id: "guest-key-1".to_string(),
},
};
let json = serde_json::to_string(&frame).unwrap();
let parsed: AuthenticatedFrame = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.version, 2);
assert_eq!(parsed.session_id, "sess-001");
assert_eq!(parsed.sequence, 42);
assert_eq!(parsed.signed.payload, b"inner request json");
assert_eq!(parsed.signed.signature.len(), 64);
}
#[test]
fn test_session_hello_serde_roundtrip() {
let hello = SessionHello {
version: PROTOCOL_VERSION_AUTHENTICATED,
session_id: "sess-002".to_string(),
challenge: vec![1, 2, 3, 4, 5],
host_pubkey: vec![0u8; 32],
};
let json = serde_json::to_string(&hello).unwrap();
let parsed: SessionHello = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.version, 2);
assert_eq!(parsed.session_id, "sess-002");
assert_eq!(parsed.challenge.len(), 5);
assert_eq!(parsed.host_pubkey.len(), 32);
}
#[test]
fn test_session_hello_ack_serde_roundtrip() {
let ack = SessionHelloAck {
version: PROTOCOL_VERSION_AUTHENTICATED,
session_id: "sess-002".to_string(),
challenge_response: vec![9; 64],
guest_pubkey: vec![0u8; 32],
};
let json = serde_json::to_string(&ack).unwrap();
let parsed: SessionHelloAck = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.version, 2);
assert_eq!(parsed.session_id, "sess-002");
assert_eq!(parsed.challenge_response.len(), 64);
assert_eq!(parsed.guest_pubkey.len(), 32);
}
#[test]
fn test_security_policy_defaults() {
let policy = SecurityPolicy::default();
assert!(policy.require_auth);
assert!(policy.access.filesystem);
assert!(policy.access.network);
assert!(policy.access.build);
assert!(policy.access.host_communication);
assert!(!policy.access.debug_exec);
assert_eq!(policy.rate_limits.frames_per_second, 100);
assert_eq!(policy.rate_limits.frames_per_minute, 3000);
assert_eq!(policy.session.max_lifetime_secs, 0);
assert_eq!(policy.session.max_tasks, 0);
}
#[test]
fn test_security_policy_serde_with_defaults() {
let json = "{}";
let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
assert!(policy.require_auth);
assert!(policy.access.filesystem);
assert_eq!(policy.rate_limits.frames_per_second, 100);
}
#[test]
fn test_security_policy_serde_override() {
let json = r#"{
"require_auth": true,
"access": { "build": false, "network": false },
"rate_limits": { "frames_per_second": 50 }
}"#;
let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
assert!(policy.require_auth);
assert!(policy.access.filesystem); assert!(!policy.access.build);
assert!(!policy.access.network);
assert_eq!(policy.rate_limits.frames_per_second, 50);
assert_eq!(policy.rate_limits.frames_per_minute, 3000); }
#[test]
fn test_security_policy_full_roundtrip() {
let policy = SecurityPolicy {
require_auth: true,
access: AccessPolicy {
filesystem: false,
network: true,
build: false,
host_communication: true,
debug_exec: true,
console: false,
},
rate_limits: RateLimitPolicy {
frames_per_second: 200,
frames_per_minute: 6000,
},
session: SessionPolicy {
max_lifetime_secs: 3600,
max_tasks: 100,
},
blocklist: vec![BlocklistEntry {
pattern: "rm -rf /".to_string(),
category: "destructive".to_string(),
severity: BlocklistSeverity::Critical,
action: BlocklistAction::Block,
}],
};
let json = serde_json::to_string(&policy).unwrap();
let parsed: SecurityPolicy = serde_json::from_str(&json).unwrap();
assert!(parsed.require_auth);
assert!(!parsed.access.filesystem);
assert!(!parsed.access.build);
assert!(parsed.access.debug_exec);
assert_eq!(parsed.rate_limits.frames_per_second, 200);
assert_eq!(parsed.session.max_lifetime_secs, 3600);
assert_eq!(parsed.session.max_tasks, 100);
assert_eq!(parsed.blocklist.len(), 1);
assert_eq!(parsed.blocklist[0].pattern, "rm -rf /");
}
#[test]
fn test_protocol_version_constants() {
assert_eq!(PROTOCOL_VERSION_AUTHENTICATED, 2);
assert_eq!(PROTOCOL_VERSION_LEGACY, 1);
}
#[test]
fn test_gate_decision_serde_roundtrip() {
let decisions = vec![
GateDecision::Allow,
GateDecision::Blocked {
pattern: "rm -rf /".to_string(),
reason: "destructive".to_string(),
},
GateDecision::RequiresApproval {
reason: "matched pattern: nsenter".to_string(),
},
];
for decision in decisions {
let json = serde_json::to_string(&decision).unwrap();
let parsed: GateDecision = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, decision);
}
}
#[test]
fn test_approval_verdict_serde_roundtrip() {
let verdicts = vec![
ApprovalVerdict::Approved,
ApprovalVerdict::Denied {
reason: "policy violation".to_string(),
},
ApprovalVerdict::Timeout,
];
for verdict in verdicts {
let json = serde_json::to_string(&verdict).unwrap();
let parsed: ApprovalVerdict = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, verdict);
}
}
#[test]
fn test_blocklist_entry_serde_roundtrip() {
let entry = BlocklistEntry {
pattern: "rm -rf /".to_string(),
category: "destructive".to_string(),
severity: BlocklistSeverity::Critical,
action: BlocklistAction::Block,
};
let json = serde_json::to_string(&entry).unwrap();
let parsed: BlocklistEntry = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.pattern, "rm -rf /");
assert_eq!(parsed.severity, BlocklistSeverity::Critical);
assert_eq!(parsed.action, BlocklistAction::Block);
}
#[test]
fn test_blocklist_severity_ordering() {
assert!(BlocklistSeverity::Low < BlocklistSeverity::Medium);
assert!(BlocklistSeverity::Medium < BlocklistSeverity::High);
assert!(BlocklistSeverity::High < BlocklistSeverity::Critical);
}
#[test]
fn test_blocklist_action_values() {
let actions = vec![
BlocklistAction::Block,
BlocklistAction::RequireApproval,
BlocklistAction::Log,
];
for action in actions {
let json = serde_json::to_string(&action).unwrap();
let parsed: BlocklistAction = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, action);
}
}
#[test]
fn test_security_policy_with_blocklist() {
let json = r#"{
"require_auth": true,
"blocklist": [
{
"pattern": "rm -rf /",
"category": "destructive",
"severity": "Critical",
"action": "Block"
}
]
}"#;
let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
assert!(policy.require_auth);
assert_eq!(policy.blocklist.len(), 1);
assert_eq!(policy.blocklist[0].pattern, "rm -rf /");
assert_eq!(policy.blocklist[0].action, BlocklistAction::Block);
}
#[test]
fn test_security_policy_empty_blocklist_default() {
let json = "{}";
let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
assert!(policy.blocklist.is_empty());
}
#[test]
fn test_threat_category_serde_roundtrip() {
let categories = vec![
ThreatCategory::SecretExposure,
ThreatCategory::DataExfiltration,
ThreatCategory::Injection,
ThreatCategory::Destructive,
ThreatCategory::PrivilegeEscalation,
ThreatCategory::SupplyChain,
ThreatCategory::SensitiveFileAccess,
ThreatCategory::SystemModification,
ThreatCategory::NetworkAbuse,
ThreatCategory::ToolPoisoning,
];
for cat in categories {
let json = serde_json::to_string(&cat).unwrap();
let parsed: ThreatCategory = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, cat);
}
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Info < Severity::Low);
assert!(Severity::Low < Severity::Medium);
assert!(Severity::Medium < Severity::High);
assert!(Severity::High < Severity::Critical);
}
#[test]
fn test_severity_serde_roundtrip() {
let severities = vec![
Severity::Info,
Severity::Low,
Severity::Medium,
Severity::High,
Severity::Critical,
];
for sev in severities {
let json = serde_json::to_string(&sev).unwrap();
let parsed: Severity = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, sev);
}
}
#[test]
fn test_threat_finding_serde_roundtrip() {
let finding = ThreatFinding {
category: ThreatCategory::SecretExposure,
pattern_id: "aws_access_key".to_string(),
severity: Severity::Critical,
matched_text: "AKIAIOSFODNN7EXAMPLE".to_string(),
context: "AWS access key detected".to_string(),
};
let json = serde_json::to_string(&finding).unwrap();
let parsed: ThreatFinding = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.category, ThreatCategory::SecretExposure);
assert_eq!(parsed.pattern_id, "aws_access_key");
assert_eq!(parsed.severity, Severity::Critical);
}
#[test]
fn test_security_layer_all_count() {
assert_eq!(SecurityLayer::all().len(), 12);
}
#[test]
fn test_security_layer_serde_roundtrip() {
for layer in SecurityLayer::all() {
let json = serde_json::to_string(layer).unwrap();
let parsed: SecurityLayer = serde_json::from_str(&json).unwrap();
assert_eq!(&parsed, layer);
}
}
#[test]
fn test_posture_check_serde_roundtrip() {
let check = PostureCheck {
layer: SecurityLayer::JailerIsolation,
name: "Jailer enabled".to_string(),
passed: true,
detail: "Firecracker runs inside jailer".to_string(),
};
let json = serde_json::to_string(&check).unwrap();
let parsed: PostureCheck = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.layer, SecurityLayer::JailerIsolation);
assert!(parsed.passed);
}
#[test]
fn test_posture_report_serde_roundtrip() {
let report = PostureReport {
checks: vec![
PostureCheck {
layer: SecurityLayer::CgroupLimits,
name: "cgroup v2 limits set".to_string(),
passed: true,
detail: "mem + cpu limits configured".to_string(),
},
PostureCheck {
layer: SecurityLayer::VsockAuth,
name: "vsock auth enabled".to_string(),
passed: false,
detail: "require_auth is false".to_string(),
},
],
score: 50.0,
timestamp: "2026-02-25T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&report).unwrap();
let parsed: PostureReport = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.checks.len(), 2);
assert_eq!(parsed.score, 50.0);
}
}