diff --git a/contracts/src/auditing.rs b/contracts/src/auditing.rs new file mode 100644 index 00000000..6adaa4e5 --- /dev/null +++ b/contracts/src/auditing.rs @@ -0,0 +1,118 @@ +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Symbol, Vec}; + +use crate::errors::Error; +use crate::events::emit_audit_event; +use crate::storage::AuditDataKey; +use crate::types::AuditRecord; + +#[contract] +pub struct Auditing; + +#[contractimpl] +impl Auditing { + pub fn get_audit_report(env: Env, credential_id: BytesN<32>) -> Vec { + env.storage() + .persistent() + .get(&AuditDataKey::AuditRecords(credential_id)) + .unwrap_or(Vec::new(&env)) + } + + pub fn log_audit_event( + env: Env, + credential_id: BytesN<32>, + actor: Address, + action: Symbol, + details: String, + ) { + let timestamp = env.ledger().timestamp(); + let record = AuditRecord { + credential_id: credential_id.clone(), + actor: actor.clone(), + action: action.clone(), + timestamp, + details, + }; + + let mut records = Self::get_audit_report(env.clone(), credential_id.clone()); + records.push_back(record.clone()); + + env.storage() + .persistent() + .set(&AuditDataKey::AuditRecords(credential_id.clone()), &records); + + emit_audit_event(&env, actor, record); + } + + pub fn track_activity(env: Env, actor: Address, is_failure: bool) -> Result<(), Error> { + let mut failed_attempts: u32 = env + .storage() + .instance() + .get(&AuditDataKey::ActivityTracker(actor.clone())) + .unwrap_or(0); + + if is_failure { + failed_attempts += 1; + } else { + failed_attempts = 0; + } + + env.storage().instance().set( + &AuditDataKey::ActivityTracker(actor.clone()), + &failed_attempts, + ); + + if failed_attempts > 5 { + Self::log_audit_event( + env.clone(), + BytesN::from_array(&env, &[0; 32]), // Dummy ID for suspicious actor without specific credential + actor.clone(), + Symbol::new(&env, "suspicious"), + String::from_str(&env, "Excessive verification failures"), + ); + } + + Ok(()) + } + + pub fn require_auth(_env: Env, actor: Address) { + actor.require_auth(); + } + + pub fn credential_exists(env: Env, credential_id: BytesN<32>) -> Result<(), Error> { + let records: Option> = env + .storage() + .persistent() + .get(&AuditDataKey::AuditRecords(credential_id)); + if records.is_none() { + return Err(Error::CredentialNotFound); + } + Ok(()) + } + + pub fn credential_not_revoked(env: Env, credential_id: BytesN<32>) -> Result<(), Error> { + let records = Self::get_audit_report(env.clone(), credential_id); + for record in records.iter() { + if record.action == Symbol::new(&env, "revoked") { + return Err(Error::CredentialRevoked); + } + } + Ok(()) + } + + pub fn issuer_authorized( + env: Env, + issuer: Address, + credential_id: BytesN<32>, + ) -> Result<(), Error> { + let records = Self::get_audit_report(env.clone(), credential_id); + for record in records.iter() { + if record.action == Symbol::new(&env, "issued") { + if record.actor != issuer { + return Err(Error::IssuerNotAuthorized); + } + return Ok(()); + } + } + Ok(()) + } +} diff --git a/contracts/src/errors.rs b/contracts/src/errors.rs index d90594e7..09be593e 100644 --- a/contracts/src/errors.rs +++ b/contracts/src/errors.rs @@ -21,4 +21,9 @@ pub enum Error { MigrationFailed = 14, VersionMismatch = 15, StorageCorrupted = 16, + // Auditing errors + SuspiciousActivity = 17, + IssuerNotAuthorized = 18, + CredentialRevoked = 19, + CredentialNotFound = 20, } diff --git a/contracts/src/events.rs b/contracts/src/events.rs new file mode 100644 index 00000000..826848b4 --- /dev/null +++ b/contracts/src/events.rs @@ -0,0 +1,7 @@ +use crate::types::AuditRecord; +use soroban_sdk::{symbol_short, Address, Env}; + +pub fn emit_audit_event(env: &Env, actor: Address, audit_record: AuditRecord) { + env.events() + .publish((symbol_short!("audit"), actor), audit_record); +} diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 8326ce1f..b3dc34c0 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -1,9 +1,13 @@ #![no_std] pub mod access_control; +pub mod auditing; pub mod data_sharing; pub mod errors; +pub mod events; pub mod identity_registry; +pub mod storage; +pub mod types; pub mod upgrade; pub mod verification; @@ -16,6 +20,9 @@ mod integration_tests; #[cfg(test)] mod benchmarks; +#[cfg(test)] +mod test; + pub use access_control::AccessControl; pub use data_sharing::DataSharing; pub use errors::Error; diff --git a/contracts/src/storage.rs b/contracts/src/storage.rs new file mode 100644 index 00000000..252bc0c1 --- /dev/null +++ b/contracts/src/storage.rs @@ -0,0 +1,7 @@ +use soroban_sdk::{contracttype, Address, BytesN}; + +#[contracttype] +pub enum AuditDataKey { + AuditRecords(BytesN<32>), + ActivityTracker(Address), +} diff --git a/contracts/src/test.rs b/contracts/src/test.rs new file mode 100644 index 00000000..171344f4 --- /dev/null +++ b/contracts/src/test.rs @@ -0,0 +1,101 @@ +#![cfg(test)] + +use crate::auditing::{Auditing, AuditingClient}; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Symbol}; + +#[test] +fn test_audit_record_created() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + let credential_id = BytesN::from_array(&env, &[1; 32]); + + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "issued"), + &String::from_str(&env, "Credential created"), + ); + + let report = client.get_audit_report(&credential_id); + assert_eq!(report.len(), 1); + let record = report.get(0).unwrap(); + assert_eq!(record.credential_id, credential_id); + assert_eq!(record.actor, actor); + assert_eq!(record.action, Symbol::new(&env, "issued")); +} + +#[test] +fn test_suspicious_activity_logged() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + + for _ in 0..5 { + client.track_activity(&actor, &true); + } + + // 6th failure should trigger suspicious activity + client.track_activity(&actor, &true); + + // Verify dummy credential ID was used for the event + let dummy_id = BytesN::from_array(&env, &[0; 32]); + let report = client.get_audit_report(&dummy_id); + assert_eq!(report.len(), 1); + let record = report.get(0).unwrap(); + assert_eq!(record.action, Symbol::new(&env, "suspicious")); +} + +#[test] +fn test_security_check_failure() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let credential_id = BytesN::from_array(&env, &[2; 32]); + + let res = client.try_credential_exists(&credential_id); + assert!(res.is_err()); +} + +#[test] +fn test_audit_report_generation() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + let credential_id = BytesN::from_array(&env, &[3; 32]); + + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "issued"), + &String::from_str(&env, "first"), + ); + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "verified"), + &String::from_str(&env, "second"), + ); + + let report = client.get_audit_report(&credential_id); + assert_eq!(report.len(), 2); +} + +#[test] +fn test_events_emitted() { + let env = Env::default(); + let contract_id = env.register_contract(None, Auditing); + let client = AuditingClient::new(&env, &contract_id); + let actor = Address::generate(&env); + let credential_id = BytesN::from_array(&env, &[4; 32]); + + client.log_audit_event( + &credential_id, + &actor, + &Symbol::new(&env, "issued"), + &String::from_str(&env, "Emit test"), + ); +} diff --git a/contracts/src/types.rs b/contracts/src/types.rs new file mode 100644 index 00000000..d60784d8 --- /dev/null +++ b/contracts/src/types.rs @@ -0,0 +1,21 @@ +use soroban_sdk::{contracttype, Address, BytesN, String, Symbol}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AuditEventType { + CredentialIssued, + CredentialUpdated, + CredentialRevoked, + CredentialVerified, + SuspiciousActivityDetected, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AuditRecord { + pub credential_id: BytesN<32>, + pub actor: Address, + pub action: Symbol, + pub timestamp: u64, + pub details: String, +}