1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fs::{File, OpenOptions};
6use std::io::{BufRead, BufReader, Write};
7use std::path::Path;
8use uuid::Uuid;
9
10use crate::crypto::{hash_entry, KeyPair, signable};
11use crate::errors::{AAPError, Result};
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum AuditResult {
17 Success,
18 Failure,
19 Blocked,
20 Revoked,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct AuditEntry {
26 pub aap_version: String,
27 pub entry_id: String,
28 pub prev_hash: String,
29 pub agent_id: String,
30 pub action: String,
31 pub result: AuditResult,
32 pub timestamp: DateTime<Utc>,
33 pub provenance_id: String,
34 pub authorization_level: u8,
35 pub physical: bool,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub result_detail: Option<String>,
38 pub signature: String,
39}
40
41pub struct AuditChain {
43 entries: Vec<AuditEntry>,
44 storage_path: Option<String>,
45}
46
47impl AuditChain {
48 pub fn new() -> Self {
50 Self { entries: Vec::new(), storage_path: None }
51 }
52
53 pub fn with_storage(path: impl AsRef<Path>) -> Result<Self> {
56 let path_str = path.as_ref().to_string_lossy().to_string();
57 let mut entries = Vec::new();
58
59 if path.as_ref().exists() {
60 let f = File::open(&path)
61 .map_err(|e| AAPError::Signature(format!("cannot open audit file: {e}")))?;
62 for line in BufReader::new(f).lines() {
63 let line = line.map_err(|e| AAPError::Signature(e.to_string()))?;
64 if !line.trim().is_empty() {
65 let entry: AuditEntry = serde_json::from_str(&line)?;
66 entries.push(entry);
67 }
68 }
69 }
70
71 Ok(Self { entries, storage_path: Some(path_str) })
72 }
73
74 pub fn append(
76 &mut self,
77 agent_id: &str,
78 action: &str,
79 result: AuditResult,
80 provenance_id: &str,
81 agent_kp: &KeyPair,
82 authorization_level: u8,
83 physical: bool,
84 ) -> Result<&AuditEntry> {
85 let prev_hash = self.last_hash();
86
87 let mut entry = AuditEntry {
88 aap_version: "0.1".into(),
89 entry_id: Uuid::new_v4().to_string(),
90 prev_hash,
91 agent_id: agent_id.into(),
92 action: action.into(),
93 result,
94 timestamp: Utc::now(),
95 provenance_id: provenance_id.into(),
96 authorization_level,
97 physical,
98 result_detail: None,
99 signature: String::new(),
100 };
101
102 let v = serde_json::to_value(&entry)?;
103 let data = signable(&v)?;
104 entry.signature = agent_kp.sign(&data);
105
106 if let Some(ref path) = self.storage_path {
107 let mut f = OpenOptions::new()
108 .create(true).append(true).open(path)
109 .map_err(|e| AAPError::Signature(format!("cannot write audit: {e}")))?;
110 let line = serde_json::to_string(&entry)?;
111 writeln!(f, "{}", line)
112 .map_err(|e| AAPError::Signature(e.to_string()))?;
113 }
114
115 self.entries.push(entry);
116 Ok(self.entries.last().unwrap())
117 }
118
119 pub fn verify(&self) -> (bool, usize, Option<String>) {
122 let mut prev_hash = "genesis".to_string();
123
124 for (i, entry) in self.entries.iter().enumerate() {
125 if entry.prev_hash != prev_hash {
126 return (false, i, Some(entry.entry_id.clone()));
127 }
128 let v = serde_json::to_value(entry).unwrap_or_default();
129 prev_hash = hash_entry(&v);
130 }
131 (true, self.entries.len(), None)
132 }
133
134 pub fn len(&self) -> usize { self.entries.len() }
135 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
136 pub fn entries(&self) -> &[AuditEntry] { &self.entries }
137
138 fn last_hash(&self) -> String {
139 match self.entries.last() {
140 None => "genesis".to_string(),
141 Some(e) => {
142 let v = serde_json::to_value(e).unwrap_or_default();
143 hash_entry(&v)
144 }
145 }
146 }
147}
148
149impl Default for AuditChain {
150 fn default() -> Self { Self::new() }
151}