1use sha1::{Digest, Sha1};
2
3use serde::Serialize;
4
5use crate::core::safety::DecisionRecord;
6
7fn digest(bytes: &[u8]) -> String {
8 let mut h = Sha1::new();
9 h.update(bytes);
10 hex::encode(h.finalize())
11}
12
13#[derive(Debug, Clone, Serialize)]
16pub struct AuditEntry {
17 pub seq: u64,
18 pub prev_hash: String,
19 pub hash: String,
20 #[serde(flatten)]
21 pub record: DecisionRecord,
22}
23
24impl AuditEntry {
25 pub fn new(seq: u64, prev_hash: &str, record: DecisionRecord) -> Self {
26 let payload = format!("{prev_hash}|{}", serde_json::to_string(&record).unwrap_or_default());
27 let hash = digest(payload.as_bytes());
28 AuditEntry {
29 seq,
30 prev_hash: prev_hash.to_string(),
31 hash,
32 record,
33 }
34 }
35
36 fn chain_bytes(&self) -> Vec<u8> {
37 format!(
38 "{}|{}",
39 self.prev_hash,
40 serde_json::to_string(&self.record).unwrap_or_default()
41 )
42 .into_bytes()
43 }
44}
45
46#[derive(Debug, Clone, Default, Serialize)]
50pub struct HashChain {
51 entries: Vec<AuditEntry>,
52 head: String,
53}
54
55impl HashChain {
56 pub fn new() -> Self {
57 HashChain {
58 entries: Vec::new(),
59 head: "0".repeat(40),
60 }
61 }
62
63 pub fn append(&mut self, record: DecisionRecord) -> u64 {
64 let seq = (self.entries.len() as u64) + 1;
65 let entry = AuditEntry::new(seq, &self.head, record);
66 self.head = entry.hash.clone();
67 self.entries.push(entry);
68 seq
69 }
70
71 pub fn len(&self) -> usize {
72 self.entries.len()
73 }
74
75 pub fn is_empty(&self) -> bool {
76 self.entries.is_empty()
77 }
78
79 pub fn records(&self) -> Vec<DecisionRecord> {
80 self.entries.iter().map(|e| e.record.clone()).collect()
81 }
82
83 pub fn last_record(&self) -> Option<&DecisionRecord> {
84 self.entries.last().map(|e| &e.record)
85 }
86
87 pub fn entries(&self) -> &[AuditEntry] {
88 &self.entries
89 }
90
91 pub fn recent(&self, n: usize) -> Vec<DecisionRecord> {
92 let end = self.entries.len();
93 let start = end.saturating_sub(n);
94 self.entries[start..].iter().map(|e| e.record.clone()).collect()
95 }
96
97 pub fn tip_hex(&self) -> String {
99 self.head.clone()
100 }
101
102 pub fn verify(&self) -> bool {
105 let mut prev = "0".repeat(40);
106 for e in &self.entries {
107 if e.seq == 0 || e.prev_hash != prev {
108 return false;
109 }
110 let expected = digest(&e.chain_bytes());
111 if e.hash != expected {
112 return false;
113 }
114 prev = e.hash.clone();
115 }
116 true
117 }
118}
119
120impl From<&HashChain> for Vec<DecisionRecord> {
121 fn from(c: &HashChain) -> Self {
122 c.records()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::core::module::{Capability, Intent};
130 use crate::core::safety::{PolicyContext, PolicyDecision, RiskLevel};
131
132 fn fake_record(seq: u64) -> DecisionRecord {
133 DecisionRecord {
134 at: seq,
135 target: format!("10.0.0.{}", seq),
136 module: "test".into(),
137 capabilities: vec![Capability::NetworkScan],
138 intents: vec![Intent::Read],
139 impact: RiskLevel::Low,
140 context: PolicyContext::Cli,
141 decision: PolicyDecision::Allow,
142 }
143 }
144
145 #[test]
146 fn empty_chain_verifies() {
147 let c = HashChain::new();
148 assert!(c.verify());
149 assert!(c.is_empty());
150 assert_eq!(c.len(), 0);
151 }
152
153 #[test]
154 fn single_entry_verifies() {
155 let mut c = HashChain::new();
156 c.append(fake_record(1));
157 assert!(c.verify());
158 assert_eq!(c.len(), 1);
159 assert_eq!(c.records().len(), 1);
160 }
161
162 #[test]
163 fn chain_of_three_verifies() {
164 let mut c = HashChain::new();
165 for i in 1..=3 {
166 c.append(fake_record(i));
167 }
168 assert!(c.verify());
169 assert_eq!(c.len(), 3);
170 assert_eq!(c.recent(2).len(), 2);
171 assert_eq!(c.recent(10).len(), 3);
172 }
173
174 #[test]
175 fn tampered_entry_fails_verify() {
176 let mut c = HashChain::new();
177 c.append(fake_record(1));
178 c.append(fake_record(2));
179 assert!(c.verify());
180 if let Some(entry) = c.entries.last_mut() {
181 entry.record.target = "tampered".into();
182 }
183 assert!(!c.verify(), "tampered chain must fail verification");
184 }
185
186 #[test]
187 fn broken_prev_hash_fails_verify() {
188 let mut c = HashChain::new();
189 c.append(fake_record(1));
190 assert!(c.verify());
191 if let Some(entry) = c.entries.last_mut() {
192 entry.prev_hash = "badbadbad".into();
193 }
194 assert!(!c.verify());
195 }
196}