1use serde::{Deserialize, Serialize};
34use sha2::{Digest, Sha256};
35use std::fmt;
36use std::path::Path;
37use std::time::{SystemTime, UNIX_EPOCH};
38
39pub const MIN_JUSTIFICATION_LEN: usize = 8;
41
42pub const DEFAULT_AUTHORIZED_OVERRIDER: &str = "guardian";
44
45fn genesis_hash() -> [u8; 32] {
46 sha256_bytes(b"MICROSCOPE-ENFORCEMENT-AUDIT-GENESIS")
47}
48
49fn sha256_bytes(data: &[u8]) -> [u8; 32] {
50 let mut h = Sha256::new();
51 h.update(data);
52 h.finalize().into()
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct ActionEvent {
58 pub actor: String,
60 pub action: String,
62 pub content: String,
64 pub ts_ms: u64,
66 pub scope: String,
68 pub provenance: String,
70}
71
72impl ActionEvent {
73 pub fn attribution_error(&self) -> Option<String> {
76 if self.actor.trim().is_empty() {
77 return Some("faulty attribution: empty actor".to_string());
78 }
79 if self.action.trim().is_empty() {
80 return Some("faulty attribution: empty action".to_string());
81 }
82 if self.scope.trim().is_empty() {
83 return Some("faulty attribution: missing scope".to_string());
84 }
85 if self.provenance.trim().is_empty() {
86 return Some("faulty attribution: missing provenance".to_string());
87 }
88 None
89 }
90
91 pub fn new(actor: &str, action: &str, scope: &str, provenance: &str) -> Self {
93 Self {
94 actor: actor.to_string(),
95 action: action.to_string(),
96 content: String::new(),
97 ts_ms: now_ms(),
98 scope: scope.to_string(),
99 provenance: provenance.to_string(),
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct Commitment {
108 pub id: u64,
109 pub actor: String,
111 pub forbidden_action: String,
113 pub scope: String,
115 pub content: String,
117 pub created_at_ms: u64,
118 pub expires_at_ms: Option<u64>,
120}
121
122impl Commitment {
123 pub fn is_active(&self, now_ms: u64) -> bool {
126 self.expires_at_ms.is_none_or(|e| e > now_ms)
127 }
128
129 fn matches(&self, event: &ActionEvent) -> bool {
131 attribute_match(&self.actor, &event.actor)
132 && attribute_match(&self.forbidden_action, &event.action)
133 && attribute_match(&self.scope, &event.scope)
134 }
135}
136
137fn attribute_match(pattern: &str, value: &str) -> bool {
139 if pattern == "*" || pattern == value {
140 return true;
141 }
142 if !pattern.contains(['*', '?']) {
143 return false;
144 }
145 glob_match(pattern, value)
146}
147
148fn glob_match(pattern: &str, text: &str) -> bool {
150 let p: Vec<char> = pattern.chars().collect();
151 let t: Vec<char> = text.chars().collect();
152 glob_inner(&p, &t)
153}
154
155fn glob_inner(p: &[char], s: &[char]) -> bool {
156 if p.is_empty() {
157 return s.is_empty();
158 }
159 match p[0] {
160 '*' => {
161 for skip in 0..=s.len() {
162 if glob_inner(&p[1..], &s[skip..]) {
163 return true;
164 }
165 }
166 false
167 }
168 '?' => !s.is_empty() && glob_inner(&p[1..], &s[1..]),
169 c => !s.is_empty() && c == s[0] && glob_inner(&p[1..], &s[1..]),
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct Violation {
176 pub commitment: Commitment,
177 pub reason: String,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum Decision {
183 Allowed { action: String },
185 Blocked {
188 commitment: u64,
189 action: String,
190 reason: String,
191 },
192 Overridden {
195 commitment: u64,
196 action: String,
197 justification: String,
198 },
199 AttributionError { reason: String },
201}
202
203impl Decision {
204 pub fn is_valid(&self) -> bool {
206 matches!(self, Decision::Allowed { .. } | Decision::Overridden { .. })
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
212pub enum Outcome {
213 Allowed = 0,
214 Blocked = 1,
215 Overridden = 2,
216 AttributionError = 3,
217}
218
219impl Outcome {
220 fn code(self) -> u8 {
221 self as u8
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct AuditChunk {
228 pub ts_ms: u64,
229 pub outcome: Outcome,
230 pub actor: String,
231 pub action: String,
232 pub scope: String,
233 pub justification: Option<String>,
234 pub prev_hash: [u8; 32],
235 pub hash: [u8; 32],
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub struct EngineState {
241 pub commitments: Vec<Commitment>,
242 pub authorized_overriders: Vec<String>,
243 pub next_id: u64,
244}
245
246pub struct EnforcementEngine {
249 state: EngineState,
250 audit: Vec<AuditChunk>,
251}
252
253impl Default for EnforcementEngine {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl EnforcementEngine {
260 pub fn new() -> Self {
261 Self {
262 state: EngineState {
263 commitments: Vec::new(),
264 authorized_overriders: vec![DEFAULT_AUTHORIZED_OVERRIDER.to_string()],
265 next_id: 1,
266 },
267 audit: Vec::new(),
268 }
269 }
270
271 pub fn from_state(state: EngineState) -> Self {
273 Self {
274 state,
275 audit: Vec::new(),
276 }
277 }
278
279 pub fn state(&self) -> EngineState {
280 self.state.clone()
281 }
282
283 pub fn audit(&self) -> &[AuditChunk] {
284 &self.audit
285 }
286
287 pub fn restore_audit(&mut self, chunks: Vec<AuditChunk>) {
290 self.audit = chunks;
291 }
292
293 pub fn with_authorized_overrider(mut self, actor: impl Into<String>) -> Self {
296 let actor = actor.into();
297 if !self.state.authorized_overriders.contains(&actor) {
298 self.state.authorized_overriders.push(actor);
299 }
300 self
301 }
302
303 pub fn authorized_overriders(&self) -> &[String] {
304 &self.state.authorized_overriders
305 }
306
307 pub fn add_commitment(
309 &mut self,
310 actor: &str,
311 forbidden_action: &str,
312 scope: &str,
313 content: &str,
314 expires_at_ms: Option<u64>,
315 ) -> u64 {
316 let id = self.state.next_id;
317 self.state.next_id += 1;
318 self.state.commitments.push(Commitment {
319 id,
320 actor: actor.to_string(),
321 forbidden_action: forbidden_action.to_string(),
322 scope: scope.to_string(),
323 content: content.to_string(),
324 created_at_ms: now_ms(),
325 expires_at_ms,
326 });
327 id
328 }
329
330 pub fn with_commitments(mut self, commitments: Vec<Commitment>) -> Self {
332 self.state.commitments = commitments;
333 self
334 }
335
336 pub fn history(&self) -> &[Commitment] {
338 &self.state.commitments
339 }
340
341 pub fn active_commitments(&self, now_ms: u64) -> Vec<&Commitment> {
343 self.state
344 .commitments
345 .iter()
346 .filter(|c| c.is_active(now_ms))
347 .collect()
348 }
349
350 pub fn violations(&self, event: &ActionEvent) -> Vec<Violation> {
352 self.active_commitments(event.ts_ms)
353 .into_iter()
354 .filter(|c| c.matches(event))
355 .map(|c| Violation {
356 commitment: c.clone(),
357 reason: format!("action '{}' is prohibited ({})", event.action, c.content),
358 })
359 .collect()
360 }
361
362 pub fn justified_override(
366 &self,
367 event: &ActionEvent,
368 justification: &str,
369 ) -> Result<(), String> {
370 if !self
371 .state
372 .authorized_overriders
373 .iter()
374 .any(|a| a == &event.actor)
375 {
376 return Err(format!(
377 "unauthorized overrider '{}' (authorized: {})",
378 event.actor,
379 self.state.authorized_overriders.join(", ")
380 ));
381 }
382 let trimmed = justification.trim();
383 if trimmed.len() < MIN_JUSTIFICATION_LEN {
384 return Err(format!(
385 "incomplete justification ({} < {} recorded reason required)",
386 trimmed.len(),
387 MIN_JUSTIFICATION_LEN
388 ));
389 }
390 Ok(())
391 }
392
393 pub fn decide(&mut self, event: &ActionEvent, justification: Option<&str>) -> Decision {
397 if let Some(reason) = event.attribution_error() {
398 self.record(event, Outcome::AttributionError, None);
399 return Decision::AttributionError { reason };
400 }
401
402 let violations: Vec<(u64, String)> = self
405 .violations(event)
406 .into_iter()
407 .map(|v| (v.commitment.id, v.reason))
408 .collect();
409
410 if violations.is_empty() {
411 self.record(event, Outcome::Allowed, None);
412 return Decision::Allowed {
413 action: event.action.clone(),
414 };
415 }
416
417 if let Some(just) = justification {
418 match self.justified_override(event, just) {
419 Ok(()) => {
420 self.record(event, Outcome::Overridden, Some(just));
421 return Decision::Overridden {
422 commitment: violations[0].0,
423 action: event.action.clone(),
424 justification: just.to_string(),
425 };
426 }
427 Err(reason) => {
428 let blocked = format!(
429 "action '{}' violates commitment #{}; override rejected: {}",
430 event.action, violations[0].0, reason
431 );
432 self.record(event, Outcome::Blocked, None);
433 return Decision::Blocked {
434 commitment: violations[0].0,
435 action: event.action.clone(),
436 reason: blocked,
437 };
438 }
439 }
440 }
441
442 let reason = format!(
443 "action '{}' violates commitment #{} ({})",
444 event.action, violations[0].0, violations[0].1
445 );
446 self.record(event, Outcome::Blocked, None);
447 Decision::Blocked {
448 commitment: violations[0].0,
449 action: event.action.clone(),
450 reason,
451 }
452 }
453
454 pub fn can_execute(&mut self, event: &ActionEvent, justification: Option<&str>) -> bool {
461 self.decide(event, justification).is_valid()
462 }
463
464 pub fn select_valid(&mut self, candidates: &[ActionEvent]) -> Vec<ActionEvent> {
466 let decisions: Vec<Decision> = candidates.iter().map(|c| self.decide(c, None)).collect();
467 candidates
468 .iter()
469 .zip(decisions.iter())
470 .filter(|(_, d)| d.is_valid())
471 .map(|(c, _)| c.clone())
472 .collect()
473 }
474
475 pub fn chain_valid(&self) -> bool {
477 let mut prev = genesis_hash();
478 for c in &self.audit {
479 if c.prev_hash != prev {
480 return false;
481 }
482 if compute_chunk_hash(c) != c.hash {
483 return false;
484 }
485 prev = c.hash;
486 }
487 true
488 }
489
490 fn record(&mut self, event: &ActionEvent, outcome: Outcome, justification: Option<&str>) {
491 let prev_hash = self
492 .audit
493 .last()
494 .map(|c| c.hash)
495 .unwrap_or_else(genesis_hash);
496 let hash = chain_hash(
497 event.ts_ms,
498 outcome,
499 &event.actor,
500 &event.action,
501 &event.scope,
502 justification,
503 prev_hash,
504 );
505 self.audit.push(AuditChunk {
506 ts_ms: event.ts_ms,
507 outcome,
508 actor: event.actor.clone(),
509 action: event.action.clone(),
510 scope: event.scope.clone(),
511 justification: justification.map(|s| s.to_string()),
512 prev_hash,
513 hash,
514 });
515 }
516}
517
518fn chain_hash(
519 ts_ms: u64,
520 outcome: Outcome,
521 actor: &str,
522 action: &str,
523 scope: &str,
524 justification: Option<&str>,
525 prev: [u8; 32],
526) -> [u8; 32] {
527 compute_hash(ts_ms, outcome, actor, action, scope, justification, prev)
528}
529
530fn compute_chunk_hash(c: &AuditChunk) -> [u8; 32] {
531 chain_hash(
532 c.ts_ms,
533 c.outcome,
534 &c.actor,
535 &c.action,
536 &c.scope,
537 c.justification.as_deref(),
538 c.prev_hash,
539 )
540}
541
542fn compute_hash(
543 ts_ms: u64,
544 outcome: Outcome,
545 actor: &str,
546 action: &str,
547 scope: &str,
548 justification: Option<&str>,
549 prev: [u8; 32],
550) -> [u8; 32] {
551 let mut h = Sha256::new();
552 h.update(prev);
553 h.update(ts_ms.to_le_bytes());
554 h.update([outcome.code()]);
555 h.update((actor.len() as u32).to_le_bytes());
556 h.update(actor.as_bytes());
557 h.update((action.len() as u32).to_le_bytes());
558 h.update(action.as_bytes());
559 h.update((scope.len() as u32).to_le_bytes());
560 h.update(scope.as_bytes());
561 match justification {
562 Some(j) => {
563 h.update([1u8]);
564 h.update((j.len() as u32).to_le_bytes());
565 h.update(j.as_bytes());
566 }
567 None => {
568 h.update([0u8]);
569 }
570 }
571 h.finalize().into()
572}
573
574pub fn save_engine(output_dir: &Path, engine: &EnforcementEngine) -> Result<(), String> {
576 let bytes =
577 bincode::serialize(&engine.state).map_err(|e| format!("serialize enforcement: {}", e))?;
578 std::fs::write(output_dir.join("enforcement-state.bin"), bytes)
579 .map_err(|e| format!("save enforcement-state: {}", e))
580}
581
582pub fn load_engine(output_dir: &Path) -> Result<EnforcementEngine, String> {
584 let mut engine = {
585 let path = output_dir.join("enforcement-state.bin");
586 if !path.exists() {
587 EnforcementEngine::new()
588 } else {
589 let bytes =
590 std::fs::read(&path).map_err(|e| format!("read enforcement-state: {}", e))?;
591 let state: EngineState = bincode::deserialize(&bytes)
592 .map_err(|e| format!("deserialize enforcement-state: {}", e))?;
593 EnforcementEngine::from_state(state)
594 }
595 };
596 if let Ok(chunks) = load_audit(output_dir) {
599 engine.restore_audit(chunks);
600 }
601 Ok(engine)
602}
603
604pub fn load_engine_strict(output_dir: &Path) -> Result<EnforcementEngine, String> {
609 let state_path = output_dir.join("enforcement-state.bin");
610 let has_state = state_path.exists();
611 let audit_path = output_dir.join("enforcement-audit.bin");
612 let has_audit = audit_path.exists();
613
614 if (has_audit || has_state) && !output_dir.is_dir() {
615 return Err("enforcement state dir is not a directory".to_string());
616 }
617
618 let engine = if has_state {
619 load_engine(output_dir)?
620 } else {
621 EnforcementEngine::new()
622 };
623
624 if has_audit {
625 let chunks =
627 load_audit(output_dir).map_err(|e| format!("enforcement audit unreadable: {e}"))?;
628 if !chunks.is_empty() {
630 let mut probe = EnforcementEngine::new();
631 probe.restore_audit(chunks);
632 if !probe.chain_valid() {
633 return Err("enforcement audit chain is invalid".to_string());
634 }
635 }
636 }
637
638 if !engine.chain_valid() {
639 return Err("enforcement audit chain is invalid".to_string());
640 }
641 Ok(engine)
642}
643
644pub fn save_audit(output_dir: &Path, chunks: &[AuditChunk]) -> Result<(), String> {
646 let bytes = bincode::serialize(chunks).map_err(|e| format!("serialize audit: {}", e))?;
647 let mut out = Vec::with_capacity(4 + bytes.len());
648 out.extend_from_slice(b"EAU1");
649 out.extend_from_slice(&bytes);
650 std::fs::write(output_dir.join("enforcement-audit.bin"), out)
651 .map_err(|e| format!("save audit: {}", e))
652}
653
654pub fn load_audit(output_dir: &Path) -> Result<Vec<AuditChunk>, String> {
656 let path = output_dir.join("enforcement-audit.bin");
657 if !path.exists() {
658 return Ok(Vec::new());
659 }
660 let data = std::fs::read(&path).map_err(|e| format!("read audit: {}", e))?;
661 if data.len() < 4 || &data[0..4] != b"EAU1" {
662 return Err("enforcement-audit.bin is not a valid EAU1 file".to_string());
663 }
664 bincode::deserialize(&data[4..]).map_err(|e| format!("deserialize audit: {}", e))
665}
666
667fn now_ms() -> u64 {
668 SystemTime::now()
669 .duration_since(UNIX_EPOCH)
670 .unwrap_or_default()
671 .as_millis() as u64
672}
673
674#[derive(Debug, Clone, PartialEq, Eq)]
676pub enum EnforcementError {
677 Blocked { action: String, reason: String },
679 FaultyAttribution { reason: String },
681}
682
683impl fmt::Display for EnforcementError {
684 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
685 match self {
686 Self::Blocked { action, reason } => {
687 write!(f, "blocked '{}': {}", action, reason)
688 }
689 Self::FaultyAttribution { reason } => {
690 write!(f, "rejected: {}", reason)
691 }
692 }
693 }
694}
695
696impl std::error::Error for EnforcementError {}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 fn event(actor: &str, action: &str, scope: &str, ts: u64, provenance: &str) -> ActionEvent {
703 ActionEvent {
704 actor: actor.to_string(),
705 action: action.to_string(),
706 content: "test content".to_string(),
707 ts_ms: ts,
708 scope: scope.to_string(),
709 provenance: provenance.to_string(),
710 }
711 }
712
713 #[test]
714 fn honored_commitment_is_allowed() {
715 let mut eng = EnforcementEngine::new();
716 eng.add_commitment("lea", "delete", "db:prod", "never delete prod", None);
717 let e = event(
718 "lea",
719 "append",
720 "db:prod",
721 1_800_000_000_000,
722 "planner/step",
723 );
724 assert_eq!(
725 eng.decide(&e, None),
726 Decision::Allowed {
727 action: "append".into()
728 }
729 );
730 }
731
732 #[test]
733 fn violated_commitment_is_blocked() {
734 let mut eng = EnforcementEngine::new();
735 eng.add_commitment("lea", "delete", "db:prod", "never delete prod", None);
736 let e = event(
737 "lea",
738 "delete",
739 "db:prod",
740 1_800_000_000_000,
741 "planner/step",
742 );
743 let d = eng.decide(&e, None);
744 assert!(!d.is_valid());
745 match d {
746 Decision::Blocked {
747 commitment, action, ..
748 } => {
749 assert_eq!(action, "delete");
750 assert_eq!(commitment, 1);
751 }
752 other => panic!("expected Blocked, got {:?}", other),
753 }
754 }
755
756 #[test]
757 fn faulty_attribution_is_rejected() {
758 let mut eng = EnforcementEngine::new();
759 let bad = ActionEvent {
760 actor: " ".to_string(),
761 action: "ship".to_string(),
762 content: String::new(),
763 ts_ms: 1_800_000_000_000,
764 scope: "prod".to_string(),
765 provenance: "cli".to_string(),
766 };
767 match eng.decide(&bad, None) {
768 Decision::AttributionError { reason } => assert!(reason.contains("actor")),
769 other => panic!("expected attribution error, got {:?}", other),
770 }
771 let no_scope = event("lea", "ship", "", 1, "cli");
772 assert!(no_scope.attribution_error().is_some());
773 }
774
775 #[test]
776 fn expired_commitment_is_not_enforced() {
777 let mut eng = EnforcementEngine::new();
778 let now = 1_800_000_000_000;
779 eng.add_commitment("lea", "ship", "prod", "hold releases", Some(now - 1));
780 assert!(
781 eng.active_commitments(now).is_empty(),
782 "expired must leave K_t"
783 );
784 let e = event("lea", "ship", "prod", now, "planner/step");
785 assert_eq!(
786 eng.decide(&e, None),
787 Decision::Allowed {
788 action: "ship".into()
789 }
790 );
791 }
792
793 #[test]
794 fn legitimate_override_is_granted() {
795 let mut eng = EnforcementEngine::new();
796 eng.add_commitment("*", "ship", "prod", "manual review before prod", None);
797 let e = event(
798 "guardian",
799 "ship",
800 "prod",
801 1_800_000_000_000,
802 "planner/step",
803 );
804 let d = eng.decide(&e, Some("documented incident override approved"));
805 assert!(d.is_valid(), "authorized override must be valid: {:?}", d);
806 match d {
807 Decision::Overridden { action, .. } => assert_eq!(action, "ship"),
808 other => panic!("expected Overridden, got {:?}", other),
809 }
810 }
811
812 #[test]
813 fn unauthorized_override_is_blocked() {
814 let mut eng = EnforcementEngine::new();
815 eng.add_commitment("*", "ship", "prod", "manual review before prod", None);
816 let stranger = event("intruder", "ship", "prod", 1_800_000_000_000, "cli");
818 let d = eng.decide(&stranger, Some("a documented override request!"));
819 assert!(!d.is_valid());
820 match d {
821 Decision::Blocked { reason, .. } => assert!(reason.contains("unauthorized")),
822 other => panic!("expected Blocked, got {:?}", other),
823 }
824
825 let silent = event("guardian", "ship", "prod", 1_800_000_000_000, "cli");
827 let d2 = eng.decide(&silent, None);
828 assert!(!d2.is_valid());
829 }
830
831 #[test]
832 fn select_valid_restricts_to_s_valid() {
833 let mut eng = EnforcementEngine::new();
834 eng.add_commitment("lea", "delete", "db:prod", "protect prod", None);
835 let ok = event("lea", "append", "db:prod", 1_800_000_000_000, "step");
836 let bad = event("lea", "delete", "db:prod", 1_800_000_000_001, "step");
837 let valid = eng.select_valid(&[ok.clone(), bad.clone()]);
838 assert_eq!(valid.len(), 1);
839 assert_eq!(valid[0], ok);
840 }
841
842 #[test]
843 fn audit_chain_records_every_decision_and_stays_valid() {
844 let mut eng = EnforcementEngine::new();
845 eng.add_commitment("*", "erase", "*", "never erase", None);
846 let t = 1_800_000_000_000;
847 eng.decide(&event("lea", "append", "*", t, "step"), None);
848 eng.decide(&event("lea", "erase", "*", t + 1, "step"), None);
849 eng.decide(
850 &event("guardian", "erase", "*", t + 2, "step"),
851 Some("documented override"),
852 );
853 eng.decide(&event("", "erase", "*", t + 3, "step"), None);
854
855 assert_eq!(eng.audit().len(), 4);
856 assert!(eng.chain_valid());
857
858 let outcomes: Vec<Outcome> = eng.audit().iter().map(|c| c.outcome).collect();
859 assert_eq!(
860 outcomes,
861 vec![
862 Outcome::Allowed,
863 Outcome::Blocked,
864 Outcome::Overridden,
865 Outcome::AttributionError
866 ]
867 );
868 }
869
870 #[test]
871 fn glob_matching_works() {
872 assert!(glob_match("*", "anything"));
873 assert!(glob_match("process_*", "process_learn"));
874 assert!(!glob_match("process_*", "finalize"));
875 assert!(glob_match("run_?o*", "run_xo_yay"));
876 assert!(!glob_match("run_?", "run_xyz"));
877 }
878
879 #[test]
880 fn audit_file_roundtrip() {
881 let dir = tempfile::tempdir().unwrap();
882 let mut eng = EnforcementEngine::new();
883 eng.add_commitment("*", "x", "*", "no x", None);
884 let t = 1_800_000_000_000;
885 eng.decide(&event("a", "y", "*", t, "cli"), None);
886 eng.decide(
887 &event("a", "x", "*", t + 1, "cli"),
888 Some("documented override"),
889 );
890 save_audit(dir.path(), eng.audit()).unwrap();
891 let loaded = load_audit(dir.path()).unwrap();
892 assert_eq!(loaded.len(), eng.audit().len());
893 assert_eq!(loaded, eng.audit().to_vec());
894
895 save_engine(dir.path(), &eng).unwrap();
896 let reloaded = load_engine(dir.path()).unwrap();
897 assert_eq!(reloaded.state(), eng.state());
898 }
899
900 #[test]
901 fn strict_loader_fails_closed_on_corruption() {
902 let dir = tempfile::tempdir().unwrap();
904 std::fs::write(dir.path().join("enforcement-state.bin"), b"not-bincode").unwrap();
905 assert!(load_engine_strict(dir.path()).is_err());
906
907 let dir2 = tempfile::tempdir().unwrap();
909 std::fs::write(dir2.path().join("enforcement-audit.bin"), b"EAU1junk").unwrap();
910 assert!(load_engine_strict(dir2.path()).is_err());
911
912 let dir3 = tempfile::tempdir().unwrap();
914 let mut eng = EnforcementEngine::new();
915 eng.decide(&event("a", "b", "c", 0, "t"), None);
916 save_audit(dir3.path(), eng.audit()).unwrap();
917 save_engine(dir3.path(), &eng).unwrap();
918 let path = dir3.path().join("enforcement-audit.bin");
919 let mut bytes = std::fs::read(&path).unwrap();
920 let last = bytes.len() - 1;
921 bytes[last] ^= 0xFF;
922 std::fs::write(&path, bytes).unwrap();
923 assert!(load_engine_strict(dir3.path()).is_err());
924
925 let dir4 = tempfile::tempdir().unwrap();
927 let mut ok = EnforcementEngine::new();
928 ok.decide(&event("lea", "append", "prod", 1, "cli"), None);
929 save_engine(dir4.path(), &ok).unwrap();
930 save_audit(dir4.path(), ok.audit()).unwrap();
931 assert!(load_engine_strict(dir4.path()).is_ok());
932 }
933}