Skip to main content

vex_core/
audit.rs

1//! Audit log types with Merkle verification (ISO 42001 / EU AI Act compliant)
2
3use crate::merkle::Hash;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8#[cfg(feature = "algoswitch")]
9use vex_algoswitch as algoswitch;
10
11/// Audit event types
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub enum AuditEventType {
14    AgentCreated,
15    AgentExecuted,
16    DebateStarted,
17    DebateRound,
18    DebateConcluded,
19    ConsensusReached,
20    ContextStored,
21    PaymentInitiated,
22    PaymentCompleted,
23    // ISO 42001 A.6 Lifecycle event types
24    PolicyUpdate,
25    ModelUpgrade,
26    AnomalousBehavior,
27    HumanOverride,
28    Custom(String),
29}
30
31/// Actor type for audit attribution (ISO 42001 A.6.2.8)
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
33pub enum ActorType {
34    /// AI agent performed the action
35    Bot(Uuid),
36    /// Human user performed the action
37    Human(String),
38    /// System/automated process
39    #[default]
40    System,
41}
42
43impl ActorType {
44    /// Pseudonymize human actor ID using SHA-256 to protect PII (ISO 42001 A.6.2.8)
45    pub fn pseudonymize(&self) -> Self {
46        match self {
47            Self::Human(id) => {
48                use sha2::{Digest, Sha256};
49                let mut hasher = Sha256::new();
50                hasher.update(id.as_bytes());
51                Self::Human(hex::encode(hasher.finalize()))
52            }
53            other => other.clone(),
54        }
55    }
56}
57
58/// Cryptographic signature for multi-party authorization (ISO 42001 A.6.1.3)
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60pub struct Signature {
61    pub signer_id: String,
62    pub signed_at: DateTime<Utc>,
63    pub signature_hex: String,
64}
65
66impl Signature {
67    pub fn create(
68        signer_id: impl Into<String>,
69        message: &[u8],
70        signing_key: &ed25519_dalek::SigningKey,
71    ) -> Self {
72        use ed25519_dalek::Signer;
73        let signature = signing_key.sign(message);
74
75        Self {
76            signer_id: signer_id.into(),
77            signed_at: Utc::now(),
78            signature_hex: hex::encode(signature.to_bytes()),
79        }
80    }
81
82    pub fn verify(
83        &self,
84        message: &[u8],
85        verifying_key: &ed25519_dalek::VerifyingKey,
86    ) -> Result<bool, String> {
87        let sig_bytes = match hex::decode(&self.signature_hex) {
88            Ok(bytes) => bytes,
89            Err(_) => return Ok(false),
90        };
91
92        let sig_array: [u8; 64] = match sig_bytes.try_into() {
93            Ok(arr) => arr,
94            Err(_) => return Ok(false),
95        };
96
97        let signature = ed25519_dalek::Signature::from_bytes(&sig_array);
98
99        match verifying_key.verify_strict(message, &signature) {
100            Ok(()) => Ok(true),
101            Err(e) => Err(format!("Signature verification failed: {}", e)),
102        }
103    }
104}
105
106/// Single audit event (ISO 42001 / EU AI Act compliant)
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct AuditEvent {
109    pub id: Uuid,
110    pub event_type: AuditEventType,
111    pub timestamp: DateTime<Utc>,
112    pub agent_id: Option<Uuid>,
113    pub data: serde_json::Value,
114    pub hash: Hash,
115    pub previous_hash: Option<Hash>,
116    pub sequence_number: u64,
117
118    // Compliance Fields
119    pub actor: ActorType,
120    pub rationale: Option<String>,
121    pub policy_version: Option<String>,
122    pub data_provenance_hash: Option<Hash>,
123    pub human_review_required: bool,
124    pub approval_signatures: Vec<Signature>,
125}
126
127/// Parameters for consistent event hashing
128pub struct HashParams<'a> {
129    pub event_type: &'a AuditEventType,
130    pub timestamp: chrono::DateTime<Utc>,
131    pub sequence_number: u64,
132    pub data: &'a serde_json::Value,
133    pub actor: &'a ActorType,
134    pub rationale: &'a Option<String>,
135    pub policy_version: &'a Option<String>,
136    pub data_provenance_hash: &'a Option<Hash>,
137    pub human_review_required: bool,
138    pub approval_count: usize,
139}
140
141impl AuditEvent {
142    /// Fields that should be redacted from audit log data for security
143    const SENSITIVE_FIELDS: &'static [&'static str] = &[
144        "password",
145        "secret",
146        "token",
147        "api_key",
148        "apikey",
149        "key",
150        "authorization",
151        "auth",
152        "credential",
153        "private_key",
154        "privatekey",
155    ];
156
157    /// Create a new audit event with sanitized data
158    pub fn new(
159        event_type: AuditEventType,
160        agent_id: Option<Uuid>,
161        data: serde_json::Value,
162        sequence_number: u64,
163    ) -> Self {
164        let id = Uuid::new_v4();
165        let timestamp = Utc::now();
166
167        // Sanitize sensitive fields from data
168        let data = Self::sanitize_data(data);
169
170        // Default ISO 42001 / EU AI Act fields
171        let actor = ActorType::System;
172        let rationale: Option<String> = None;
173        let policy_version: Option<String> = None;
174        let data_provenance_hash: Option<Hash> = None;
175        let human_review_required = false;
176        let approval_signatures: Vec<Signature> = Vec::new();
177
178        // Compute hash including ALL fields (Centralized in vex-core)
179        let hash = Self::compute_hash(HashParams {
180            event_type: &event_type,
181            timestamp,
182            sequence_number,
183            data: &data,
184            actor: &actor,
185            rationale: &rationale,
186            policy_version: &policy_version,
187            data_provenance_hash: &data_provenance_hash,
188            human_review_required,
189            approval_count: approval_signatures.len(),
190        });
191
192        Self {
193            id,
194            event_type,
195            timestamp,
196            agent_id,
197            data,
198            hash,
199            previous_hash: None,
200            sequence_number,
201            actor,
202            rationale,
203            policy_version,
204            data_provenance_hash,
205            human_review_required,
206            approval_signatures,
207        }
208    }
209
210    /// Sanitize sensitive fields from audit data (HIGH-2 fix)
211    pub fn sanitize_data(value: serde_json::Value) -> serde_json::Value {
212        match value {
213            serde_json::Value::Object(mut map) => {
214                for key in map.keys().cloned().collect::<Vec<_>>() {
215                    let lower_key = key.to_lowercase();
216                    if Self::SENSITIVE_FIELDS.iter().any(|f| lower_key.contains(f)) {
217                        map.insert(key, serde_json::Value::String("[REDACTED]".to_string()));
218                    } else if let Some(v) = map.remove(&key) {
219                        map.insert(key, Self::sanitize_data(v));
220                    }
221                }
222                serde_json::Value::Object(map)
223            }
224            serde_json::Value::Array(arr) => {
225                serde_json::Value::Array(arr.into_iter().map(Self::sanitize_data).collect())
226            }
227            other => other,
228        }
229    }
230
231    /// Create with chained previous hash
232    pub fn chained(
233        event_type: AuditEventType,
234        agent_id: Option<Uuid>,
235        data: serde_json::Value,
236        previous_hash: Hash,
237        sequence_number: u64,
238    ) -> Self {
239        let mut event = Self::new(event_type, agent_id, data, sequence_number);
240        event.previous_hash = Some(previous_hash.clone());
241        // Rehash including previous hash and sequence (Centralized in vex-core)
242        event.hash = Self::compute_chained_hash(&event.hash, &previous_hash, sequence_number);
243        event
244    }
245
246    pub fn compute_hash(params: HashParams) -> Hash {
247        let content = format!(
248            "{:?}:{}:{}:{:?}:{:?}:{:?}:{:?}:{:?}:{}:{}",
249            params.event_type,
250            params.timestamp.timestamp(),
251            params.sequence_number,
252            params.data,
253            params.actor,
254            params.rationale,
255            params.policy_version,
256            params.data_provenance_hash.as_ref().map(|h| h.to_hex()),
257            params.human_review_required,
258            params.approval_count,
259        );
260        Hash::digest(content.as_bytes())
261    }
262
263    pub fn compute_chained_hash(base_hash: &Hash, prev_hash: &Hash, sequence: u64) -> Hash {
264        let content = format!("{}:{}:{}", base_hash, prev_hash, sequence);
265        Hash::digest(content.as_bytes())
266    }
267
268    /// Optimized hash computation using AlgoSwitch for non-critical performance tracing
269    #[cfg(feature = "algoswitch")]
270    pub fn compute_optimized_hash(params: HashParams) -> u64 {
271        let content = format!(
272            "{:?}:{}:{}:{:?}:{:?}:{:?}:{}",
273            params.event_type,
274            params.timestamp.timestamp(),
275            params.sequence_number,
276            params.data,
277            params.actor,
278            params.rationale,
279            params.approval_count,
280        );
281        algoswitch::select_hash(content.as_bytes()).0
282    }
283}