1use serde::{Deserialize, Serialize};
2
3use crate::signing::SignedPayload;
4
5pub const PROTOCOL_VERSION_AUTHENTICATED: u8 = 2;
7
8pub const PROTOCOL_VERSION_LEGACY: u8 = 1;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct AuthenticatedFrame {
22 pub version: u8,
24 pub session_id: String,
26 pub sequence: u64,
28 pub timestamp: String,
30 pub signed: SignedPayload,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct SessionHello {
41 pub version: u8,
43 pub session_id: String,
45 pub challenge: Vec<u8>,
47 pub host_pubkey: Vec<u8>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SessionHelloAck {
54 pub version: u8,
56 pub session_id: String,
58 pub challenge_response: Vec<u8>,
60 pub guest_pubkey: Vec<u8>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct SecurityPolicy {
77 #[serde(default = "default_true")]
80 pub require_auth: bool,
81
82 #[serde(default)]
84 pub access: AccessPolicy,
85
86 #[serde(default)]
88 pub rate_limits: RateLimitPolicy,
89
90 #[serde(default)]
92 pub session: SessionPolicy,
93
94 #[serde(default)]
96 pub blocklist: Vec<BlocklistEntry>,
97}
98
99impl Default for SecurityPolicy {
100 fn default() -> Self {
101 Self {
102 require_auth: true,
103 access: AccessPolicy::default(),
104 rate_limits: RateLimitPolicy::default(),
105 session: SessionPolicy::default(),
106 blocklist: Vec::new(),
107 }
108 }
109}
110
111impl SecurityPolicy {
112 pub fn dev_defaults() -> Self {
115 Self {
116 require_auth: false,
117 access: AccessPolicy {
118 console: true,
119 debug_exec: true,
120 ..AccessPolicy::default()
121 },
122 ..Self::default()
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct AccessPolicy {
130 #[serde(default = "default_true")]
132 pub filesystem: bool,
133
134 #[serde(default = "default_true")]
136 pub network: bool,
137
138 #[serde(default = "default_true")]
140 pub build: bool,
141
142 #[serde(default = "default_true")]
144 pub host_communication: bool,
145
146 #[serde(default)]
148 pub debug_exec: bool,
149
150 #[serde(default)]
152 pub console: bool,
153}
154
155impl Default for AccessPolicy {
156 fn default() -> Self {
157 Self {
158 filesystem: true,
159 network: true,
160 build: true,
161 host_communication: true,
162 debug_exec: false,
163 console: false,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct RateLimitPolicy {
171 #[serde(default = "default_rate_fps")]
173 pub frames_per_second: u32,
174
175 #[serde(default = "default_rate_fpm")]
177 pub frames_per_minute: u32,
178}
179
180impl Default for RateLimitPolicy {
181 fn default() -> Self {
182 Self {
183 frames_per_second: 100,
184 frames_per_minute: 3000,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Default, Serialize, Deserialize)]
191pub struct SessionPolicy {
192 #[serde(default)]
194 pub max_lifetime_secs: u64,
195
196 #[serde(default)]
198 pub max_tasks: u64,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub enum GateDecision {
208 Allow,
210 Blocked {
212 pattern: String,
214 reason: String,
216 },
217 RequiresApproval {
219 reason: String,
221 },
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub enum ApprovalVerdict {
227 Approved,
229 Denied { reason: String },
231 Timeout,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237pub enum BlocklistAction {
238 Block,
240 RequireApproval,
242 Log,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
248pub enum BlocklistSeverity {
249 Low,
250 Medium,
251 High,
252 Critical,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct BlocklistEntry {
258 pub pattern: String,
260 pub category: String,
262 pub severity: BlocklistSeverity,
264 pub action: BlocklistAction,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
277pub enum ThreatCategory {
278 SecretExposure,
280 DataExfiltration,
282 Injection,
284 Destructive,
286 PrivilegeEscalation,
288 SupplyChain,
290 SensitiveFileAccess,
292 SystemModification,
294 NetworkAbuse,
296 ToolPoisoning,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
302pub enum Severity {
303 Info,
304 Low,
305 Medium,
306 High,
307 Critical,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct ThreatFinding {
313 pub category: ThreatCategory,
315 pub pattern_id: String,
317 pub severity: Severity,
319 pub matched_text: String,
321 pub context: String,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
331pub enum SecurityLayer {
332 JailerIsolation,
333 CgroupLimits,
334 SeccompFilter,
335 NetworkIsolation,
336 VsockAuth,
337 EncryptionAtRest,
338 EncryptionInTransit,
339 AuditLogging,
340 SecretManagement,
341 ConfigImmutability,
342 GuestHardening,
343 SupplyChainIntegrity,
344}
345
346impl SecurityLayer {
347 pub fn all() -> &'static [SecurityLayer] {
349 &[
350 SecurityLayer::JailerIsolation,
351 SecurityLayer::CgroupLimits,
352 SecurityLayer::SeccompFilter,
353 SecurityLayer::NetworkIsolation,
354 SecurityLayer::VsockAuth,
355 SecurityLayer::EncryptionAtRest,
356 SecurityLayer::EncryptionInTransit,
357 SecurityLayer::AuditLogging,
358 SecurityLayer::SecretManagement,
359 SecurityLayer::ConfigImmutability,
360 SecurityLayer::GuestHardening,
361 SecurityLayer::SupplyChainIntegrity,
362 ]
363 }
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct PostureCheck {
369 pub layer: SecurityLayer,
371 pub name: String,
373 pub passed: bool,
375 pub detail: String,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct PostureReport {
382 pub checks: Vec<PostureCheck>,
384 pub score: f64,
386 pub timestamp: String,
388}
389
390fn default_true() -> bool {
391 true
392}
393
394fn default_rate_fps() -> u32 {
395 100
396}
397
398fn default_rate_fpm() -> u32 {
399 3000
400}
401
402#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_authenticated_frame_serde_roundtrip() {
412 let frame = AuthenticatedFrame {
413 version: PROTOCOL_VERSION_AUTHENTICATED,
414 session_id: "sess-001".to_string(),
415 sequence: 42,
416 timestamp: "2026-02-25T00:00:00Z".to_string(),
417 signed: SignedPayload {
418 payload: b"inner request json".to_vec(),
419 signature: vec![0u8; 64],
420 signer_id: "guest-key-1".to_string(),
421 },
422 };
423
424 let json = serde_json::to_string(&frame).unwrap();
425 let parsed: AuthenticatedFrame = serde_json::from_str(&json).unwrap();
426
427 assert_eq!(parsed.version, 2);
428 assert_eq!(parsed.session_id, "sess-001");
429 assert_eq!(parsed.sequence, 42);
430 assert_eq!(parsed.signed.payload, b"inner request json");
431 assert_eq!(parsed.signed.signature.len(), 64);
432 }
433
434 #[test]
435 fn test_session_hello_serde_roundtrip() {
436 let hello = SessionHello {
437 version: PROTOCOL_VERSION_AUTHENTICATED,
438 session_id: "sess-002".to_string(),
439 challenge: vec![1, 2, 3, 4, 5],
440 host_pubkey: vec![0u8; 32],
441 };
442
443 let json = serde_json::to_string(&hello).unwrap();
444 let parsed: SessionHello = serde_json::from_str(&json).unwrap();
445
446 assert_eq!(parsed.version, 2);
447 assert_eq!(parsed.session_id, "sess-002");
448 assert_eq!(parsed.challenge.len(), 5);
449 assert_eq!(parsed.host_pubkey.len(), 32);
450 }
451
452 #[test]
453 fn test_session_hello_ack_serde_roundtrip() {
454 let ack = SessionHelloAck {
455 version: PROTOCOL_VERSION_AUTHENTICATED,
456 session_id: "sess-002".to_string(),
457 challenge_response: vec![9; 64],
458 guest_pubkey: vec![0u8; 32],
459 };
460
461 let json = serde_json::to_string(&ack).unwrap();
462 let parsed: SessionHelloAck = serde_json::from_str(&json).unwrap();
463
464 assert_eq!(parsed.version, 2);
465 assert_eq!(parsed.session_id, "sess-002");
466 assert_eq!(parsed.challenge_response.len(), 64);
467 assert_eq!(parsed.guest_pubkey.len(), 32);
468 }
469
470 #[test]
471 fn test_security_policy_defaults() {
472 let policy = SecurityPolicy::default();
473
474 assert!(policy.require_auth);
475 assert!(policy.access.filesystem);
476 assert!(policy.access.network);
477 assert!(policy.access.build);
478 assert!(policy.access.host_communication);
479 assert!(!policy.access.debug_exec);
480 assert_eq!(policy.rate_limits.frames_per_second, 100);
481 assert_eq!(policy.rate_limits.frames_per_minute, 3000);
482 assert_eq!(policy.session.max_lifetime_secs, 0);
483 assert_eq!(policy.session.max_tasks, 0);
484 }
485
486 #[test]
487 fn test_security_policy_serde_with_defaults() {
488 let json = "{}";
490 let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
491
492 assert!(policy.require_auth);
493 assert!(policy.access.filesystem);
494 assert_eq!(policy.rate_limits.frames_per_second, 100);
495 }
496
497 #[test]
498 fn test_security_policy_serde_override() {
499 let json = r#"{
500 "require_auth": true,
501 "access": { "build": false, "network": false },
502 "rate_limits": { "frames_per_second": 50 }
503 }"#;
504 let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
505
506 assert!(policy.require_auth);
507 assert!(policy.access.filesystem); assert!(!policy.access.build);
509 assert!(!policy.access.network);
510 assert_eq!(policy.rate_limits.frames_per_second, 50);
511 assert_eq!(policy.rate_limits.frames_per_minute, 3000); }
513
514 #[test]
515 fn test_security_policy_full_roundtrip() {
516 let policy = SecurityPolicy {
517 require_auth: true,
518 access: AccessPolicy {
519 filesystem: false,
520 network: true,
521 build: false,
522 host_communication: true,
523 debug_exec: true,
524 console: false,
525 },
526 rate_limits: RateLimitPolicy {
527 frames_per_second: 200,
528 frames_per_minute: 6000,
529 },
530 session: SessionPolicy {
531 max_lifetime_secs: 3600,
532 max_tasks: 100,
533 },
534 blocklist: vec![BlocklistEntry {
535 pattern: "rm -rf /".to_string(),
536 category: "destructive".to_string(),
537 severity: BlocklistSeverity::Critical,
538 action: BlocklistAction::Block,
539 }],
540 };
541
542 let json = serde_json::to_string(&policy).unwrap();
543 let parsed: SecurityPolicy = serde_json::from_str(&json).unwrap();
544
545 assert!(parsed.require_auth);
546 assert!(!parsed.access.filesystem);
547 assert!(!parsed.access.build);
548 assert!(parsed.access.debug_exec);
549 assert_eq!(parsed.rate_limits.frames_per_second, 200);
550 assert_eq!(parsed.session.max_lifetime_secs, 3600);
551 assert_eq!(parsed.session.max_tasks, 100);
552 assert_eq!(parsed.blocklist.len(), 1);
553 assert_eq!(parsed.blocklist[0].pattern, "rm -rf /");
554 }
555
556 #[test]
557 fn test_protocol_version_constants() {
558 assert_eq!(PROTOCOL_VERSION_AUTHENTICATED, 2);
559 assert_eq!(PROTOCOL_VERSION_LEGACY, 1);
560 }
561
562 #[test]
563 fn test_gate_decision_serde_roundtrip() {
564 let decisions = vec![
565 GateDecision::Allow,
566 GateDecision::Blocked {
567 pattern: "rm -rf /".to_string(),
568 reason: "destructive".to_string(),
569 },
570 GateDecision::RequiresApproval {
571 reason: "matched pattern: nsenter".to_string(),
572 },
573 ];
574
575 for decision in decisions {
576 let json = serde_json::to_string(&decision).unwrap();
577 let parsed: GateDecision = serde_json::from_str(&json).unwrap();
578 assert_eq!(parsed, decision);
579 }
580 }
581
582 #[test]
583 fn test_approval_verdict_serde_roundtrip() {
584 let verdicts = vec![
585 ApprovalVerdict::Approved,
586 ApprovalVerdict::Denied {
587 reason: "policy violation".to_string(),
588 },
589 ApprovalVerdict::Timeout,
590 ];
591
592 for verdict in verdicts {
593 let json = serde_json::to_string(&verdict).unwrap();
594 let parsed: ApprovalVerdict = serde_json::from_str(&json).unwrap();
595 assert_eq!(parsed, verdict);
596 }
597 }
598
599 #[test]
600 fn test_blocklist_entry_serde_roundtrip() {
601 let entry = BlocklistEntry {
602 pattern: "rm -rf /".to_string(),
603 category: "destructive".to_string(),
604 severity: BlocklistSeverity::Critical,
605 action: BlocklistAction::Block,
606 };
607
608 let json = serde_json::to_string(&entry).unwrap();
609 let parsed: BlocklistEntry = serde_json::from_str(&json).unwrap();
610 assert_eq!(parsed.pattern, "rm -rf /");
611 assert_eq!(parsed.severity, BlocklistSeverity::Critical);
612 assert_eq!(parsed.action, BlocklistAction::Block);
613 }
614
615 #[test]
616 fn test_blocklist_severity_ordering() {
617 assert!(BlocklistSeverity::Low < BlocklistSeverity::Medium);
618 assert!(BlocklistSeverity::Medium < BlocklistSeverity::High);
619 assert!(BlocklistSeverity::High < BlocklistSeverity::Critical);
620 }
621
622 #[test]
623 fn test_blocklist_action_values() {
624 let actions = vec![
625 BlocklistAction::Block,
626 BlocklistAction::RequireApproval,
627 BlocklistAction::Log,
628 ];
629 for action in actions {
630 let json = serde_json::to_string(&action).unwrap();
631 let parsed: BlocklistAction = serde_json::from_str(&json).unwrap();
632 assert_eq!(parsed, action);
633 }
634 }
635
636 #[test]
637 fn test_security_policy_with_blocklist() {
638 let json = r#"{
639 "require_auth": true,
640 "blocklist": [
641 {
642 "pattern": "rm -rf /",
643 "category": "destructive",
644 "severity": "Critical",
645 "action": "Block"
646 }
647 ]
648 }"#;
649 let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
650
651 assert!(policy.require_auth);
652 assert_eq!(policy.blocklist.len(), 1);
653 assert_eq!(policy.blocklist[0].pattern, "rm -rf /");
654 assert_eq!(policy.blocklist[0].action, BlocklistAction::Block);
655 }
656
657 #[test]
658 fn test_security_policy_empty_blocklist_default() {
659 let json = "{}";
660 let policy: SecurityPolicy = serde_json::from_str(json).unwrap();
661 assert!(policy.blocklist.is_empty());
662 }
663
664 #[test]
667 fn test_threat_category_serde_roundtrip() {
668 let categories = vec![
669 ThreatCategory::SecretExposure,
670 ThreatCategory::DataExfiltration,
671 ThreatCategory::Injection,
672 ThreatCategory::Destructive,
673 ThreatCategory::PrivilegeEscalation,
674 ThreatCategory::SupplyChain,
675 ThreatCategory::SensitiveFileAccess,
676 ThreatCategory::SystemModification,
677 ThreatCategory::NetworkAbuse,
678 ThreatCategory::ToolPoisoning,
679 ];
680 for cat in categories {
681 let json = serde_json::to_string(&cat).unwrap();
682 let parsed: ThreatCategory = serde_json::from_str(&json).unwrap();
683 assert_eq!(parsed, cat);
684 }
685 }
686
687 #[test]
688 fn test_severity_ordering() {
689 assert!(Severity::Info < Severity::Low);
690 assert!(Severity::Low < Severity::Medium);
691 assert!(Severity::Medium < Severity::High);
692 assert!(Severity::High < Severity::Critical);
693 }
694
695 #[test]
696 fn test_severity_serde_roundtrip() {
697 let severities = vec![
698 Severity::Info,
699 Severity::Low,
700 Severity::Medium,
701 Severity::High,
702 Severity::Critical,
703 ];
704 for sev in severities {
705 let json = serde_json::to_string(&sev).unwrap();
706 let parsed: Severity = serde_json::from_str(&json).unwrap();
707 assert_eq!(parsed, sev);
708 }
709 }
710
711 #[test]
712 fn test_threat_finding_serde_roundtrip() {
713 let finding = ThreatFinding {
714 category: ThreatCategory::SecretExposure,
715 pattern_id: "aws_access_key".to_string(),
716 severity: Severity::Critical,
717 matched_text: "AKIAIOSFODNN7EXAMPLE".to_string(),
718 context: "AWS access key detected".to_string(),
719 };
720
721 let json = serde_json::to_string(&finding).unwrap();
722 let parsed: ThreatFinding = serde_json::from_str(&json).unwrap();
723 assert_eq!(parsed.category, ThreatCategory::SecretExposure);
724 assert_eq!(parsed.pattern_id, "aws_access_key");
725 assert_eq!(parsed.severity, Severity::Critical);
726 }
727
728 #[test]
731 fn test_security_layer_all_count() {
732 assert_eq!(SecurityLayer::all().len(), 12);
733 }
734
735 #[test]
736 fn test_security_layer_serde_roundtrip() {
737 for layer in SecurityLayer::all() {
738 let json = serde_json::to_string(layer).unwrap();
739 let parsed: SecurityLayer = serde_json::from_str(&json).unwrap();
740 assert_eq!(&parsed, layer);
741 }
742 }
743
744 #[test]
745 fn test_posture_check_serde_roundtrip() {
746 let check = PostureCheck {
747 layer: SecurityLayer::JailerIsolation,
748 name: "Jailer enabled".to_string(),
749 passed: true,
750 detail: "Firecracker runs inside jailer".to_string(),
751 };
752 let json = serde_json::to_string(&check).unwrap();
753 let parsed: PostureCheck = serde_json::from_str(&json).unwrap();
754 assert_eq!(parsed.layer, SecurityLayer::JailerIsolation);
755 assert!(parsed.passed);
756 }
757
758 #[test]
759 fn test_posture_report_serde_roundtrip() {
760 let report = PostureReport {
761 checks: vec![
762 PostureCheck {
763 layer: SecurityLayer::CgroupLimits,
764 name: "cgroup v2 limits set".to_string(),
765 passed: true,
766 detail: "mem + cpu limits configured".to_string(),
767 },
768 PostureCheck {
769 layer: SecurityLayer::VsockAuth,
770 name: "vsock auth enabled".to_string(),
771 passed: false,
772 detail: "require_auth is false".to_string(),
773 },
774 ],
775 score: 50.0,
776 timestamp: "2026-02-25T00:00:00Z".to_string(),
777 };
778 let json = serde_json::to_string(&report).unwrap();
779 let parsed: PostureReport = serde_json::from_str(&json).unwrap();
780 assert_eq!(parsed.checks.len(), 2);
781 assert_eq!(parsed.score, 50.0);
782 }
783}