feat(02-03): Create tamper-proof audit logger with SHA-256 hash chains
Some checks failed
Discord Webhook / git (push) Has been cancelled
Some checks failed
Discord Webhook / git (push) Has been cancelled
This commit is contained in:
6
src/audit/__init__.py
Normal file
6
src/audit/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Audit logging module for tamper-proof security event logging."""
|
||||
|
||||
from .crypto_logger import TamperProofLogger
|
||||
from .logger import AuditLogger
|
||||
|
||||
__all__ = ["TamperProofLogger", "AuditLogger"]
|
||||
327
src/audit/crypto_logger.py
Normal file
327
src/audit/crypto_logger.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""Tamper-proof logger with SHA-256 hash chains for integrity protection."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Any, Union
|
||||
import threading
|
||||
|
||||
|
||||
class TamperProofLogger:
|
||||
"""
|
||||
Tamper-proof logger using SHA-256 hash chains to detect log tampering.
|
||||
|
||||
Each log entry contains:
|
||||
- Timestamp
|
||||
- Event type and data
|
||||
- Current hash (SHA-256)
|
||||
- Previous hash (for chain integrity)
|
||||
- Cryptographic signature
|
||||
"""
|
||||
|
||||
def __init__(self, log_file: Optional[str] = None, storage_dir: str = "logs/audit"):
|
||||
"""Initialize tamper-proof logger with hash chain."""
|
||||
self.log_file = log_file or f"{storage_dir}/audit.log"
|
||||
self.storage_dir = Path(storage_dir)
|
||||
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.previous_hash: Optional[str] = None
|
||||
self.log_entries: List[Dict] = []
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Initialize hash chain from existing log if present
|
||||
self._initialize_hash_chain()
|
||||
|
||||
def _initialize_hash_chain(self) -> None:
|
||||
"""Load existing log entries and establish hash chain."""
|
||||
log_path = Path(self.log_file)
|
||||
if log_path.exists():
|
||||
try:
|
||||
with open(log_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.strip():
|
||||
entry = json.loads(line.strip())
|
||||
self.log_entries.append(entry)
|
||||
self.previous_hash = entry.get("hash")
|
||||
except (json.JSONDecodeError, IOError):
|
||||
# Start fresh if log is corrupted
|
||||
self.log_entries = []
|
||||
self.previous_hash = None
|
||||
|
||||
def _calculate_hash(
|
||||
self, event_data: Dict, previous_hash: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Calculate SHA-256 hash for event data and previous hash.
|
||||
|
||||
Args:
|
||||
event_data: Event data to hash
|
||||
previous_hash: Previous hash in chain
|
||||
|
||||
Returns:
|
||||
SHA-256 hash as hex string
|
||||
"""
|
||||
# Create canonical JSON representation
|
||||
canonical_data = {
|
||||
"timestamp": event_data.get("timestamp"),
|
||||
"event_type": event_data.get("event_type"),
|
||||
"event_data": event_data.get("event_data"),
|
||||
"previous_hash": previous_hash,
|
||||
}
|
||||
|
||||
# Sort keys for consistent hashing
|
||||
json_str = json.dumps(canonical_data, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
return hashlib.sha256(json_str.encode("utf-8")).hexdigest()
|
||||
|
||||
def _sign_hash(self, hash_value: str) -> str:
|
||||
"""
|
||||
Create cryptographic signature for hash value.
|
||||
|
||||
Args:
|
||||
hash_value: Hash to sign
|
||||
|
||||
Returns:
|
||||
Signature as hex string (simplified implementation)
|
||||
"""
|
||||
# In production, use proper asymmetric cryptography
|
||||
# For now, use HMAC with a secret key
|
||||
secret_key = "mai-audit-secret-key-change-in-production"
|
||||
return hashlib.sha256((hash_value + secret_key).encode("utf-8")).hexdigest()
|
||||
|
||||
def log_event(
|
||||
self, event_type: str, event_data: Dict, metadata: Optional[Dict] = None
|
||||
) -> str:
|
||||
"""
|
||||
Log an event with tamper-proof hash chain.
|
||||
|
||||
Args:
|
||||
event_type: Type of event (e.g., 'code_execution', 'security_assessment')
|
||||
event_data: Event-specific data
|
||||
metadata: Optional metadata (e.g., user_id, session_id)
|
||||
|
||||
Returns:
|
||||
Current hash of the logged entry
|
||||
"""
|
||||
with self.lock:
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
# Prepare event data
|
||||
log_entry_data = {
|
||||
"timestamp": timestamp,
|
||||
"event_type": event_type,
|
||||
"event_data": event_data,
|
||||
"metadata": metadata or {},
|
||||
}
|
||||
|
||||
# Calculate current hash
|
||||
current_hash = self._calculate_hash(log_entry_data, self.previous_hash)
|
||||
|
||||
# Create signature
|
||||
signature = self._sign_hash(current_hash)
|
||||
|
||||
# Create complete log entry
|
||||
log_entry = {
|
||||
"timestamp": timestamp,
|
||||
"event_type": event_type,
|
||||
"event_data": event_data,
|
||||
"metadata": metadata or {},
|
||||
"hash": current_hash,
|
||||
"previous_hash": self.previous_hash,
|
||||
"signature": signature,
|
||||
}
|
||||
|
||||
# Add to in-memory log
|
||||
self.log_entries.append(log_entry)
|
||||
self.previous_hash = current_hash
|
||||
|
||||
# Write to file
|
||||
self._write_to_file(log_entry)
|
||||
|
||||
return current_hash
|
||||
|
||||
def _write_to_file(self, log_entry: Dict) -> None:
|
||||
"""Write log entry to file."""
|
||||
try:
|
||||
log_path = Path(self.log_file)
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(log_entry) + "\n")
|
||||
except IOError as e:
|
||||
# In production, implement proper error handling and backup
|
||||
print(f"Warning: Failed to write to audit log: {e}")
|
||||
|
||||
def verify_chain(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify the integrity of the entire hash chain.
|
||||
|
||||
Returns:
|
||||
Dictionary with verification results
|
||||
"""
|
||||
results = {
|
||||
"is_valid": True,
|
||||
"total_entries": len(self.log_entries),
|
||||
"tampered_entries": [],
|
||||
"broken_links": [],
|
||||
}
|
||||
|
||||
if not self.log_entries:
|
||||
return results
|
||||
|
||||
previous_hash = None
|
||||
|
||||
for i, entry in enumerate(self.log_entries):
|
||||
# Recalculate hash
|
||||
entry_data = {
|
||||
"timestamp": entry.get("timestamp"),
|
||||
"event_type": entry.get("event_type"),
|
||||
"event_data": entry.get("event_data"),
|
||||
"previous_hash": previous_hash,
|
||||
}
|
||||
|
||||
calculated_hash = self._calculate_hash(entry_data, previous_hash)
|
||||
stored_hash = entry.get("hash")
|
||||
|
||||
if calculated_hash != stored_hash:
|
||||
results["is_valid"] = False
|
||||
results["tampered_entries"].append(
|
||||
{
|
||||
"entry_index": i,
|
||||
"timestamp": entry.get("timestamp"),
|
||||
"stored_hash": stored_hash,
|
||||
"calculated_hash": calculated_hash,
|
||||
}
|
||||
)
|
||||
|
||||
# Check hash chain continuity
|
||||
if previous_hash and entry.get("previous_hash") != previous_hash:
|
||||
results["is_valid"] = False
|
||||
results["broken_links"].append(
|
||||
{
|
||||
"entry_index": i,
|
||||
"timestamp": entry.get("timestamp"),
|
||||
"expected_previous": previous_hash,
|
||||
"actual_previous": entry.get("previous_hash"),
|
||||
}
|
||||
)
|
||||
|
||||
# Verify signature
|
||||
stored_signature = entry.get("signature")
|
||||
if stored_signature:
|
||||
expected_signature = self._sign_hash(stored_hash)
|
||||
if stored_signature != expected_signature:
|
||||
results["is_valid"] = False
|
||||
results["tampered_entries"].append(
|
||||
{
|
||||
"entry_index": i,
|
||||
"timestamp": entry.get("timestamp"),
|
||||
"issue": "Invalid signature",
|
||||
}
|
||||
)
|
||||
|
||||
previous_hash = stored_hash
|
||||
|
||||
return results
|
||||
|
||||
def get_logs(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
event_type: Optional[str] = None,
|
||||
start_time: Optional[str] = None,
|
||||
end_time: Optional[str] = None,
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Retrieve logs with optional filtering.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of entries to return
|
||||
event_type: Filter by event type
|
||||
start_time: ISO format timestamp start
|
||||
end_time: ISO format timestamp end
|
||||
|
||||
Returns:
|
||||
List of log entries
|
||||
"""
|
||||
filtered_logs = self.log_entries.copy()
|
||||
|
||||
# Filter by event type
|
||||
if event_type:
|
||||
filtered_logs = [
|
||||
log for log in filtered_logs if log.get("event_type") == event_type
|
||||
]
|
||||
|
||||
# Filter by time range
|
||||
if start_time:
|
||||
filtered_logs = [
|
||||
log for log in filtered_logs if log.get("timestamp", "") >= start_time
|
||||
]
|
||||
|
||||
if end_time:
|
||||
filtered_logs = [
|
||||
log for log in filtered_logs if log.get("timestamp", "") <= end_time
|
||||
]
|
||||
|
||||
# Apply limit
|
||||
if limit:
|
||||
filtered_logs = filtered_logs[-limit:]
|
||||
|
||||
return filtered_logs
|
||||
|
||||
def get_chain_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get information about the hash chain.
|
||||
|
||||
Returns:
|
||||
Dictionary with chain statistics
|
||||
"""
|
||||
if not self.log_entries:
|
||||
return {
|
||||
"total_entries": 0,
|
||||
"current_hash": None,
|
||||
"first_entry": None,
|
||||
"last_entry": None,
|
||||
"chain_length": 0,
|
||||
}
|
||||
|
||||
return {
|
||||
"total_entries": len(self.log_entries),
|
||||
"current_hash": self.previous_hash,
|
||||
"first_entry": {
|
||||
"timestamp": self.log_entries[0].get("timestamp"),
|
||||
"hash": self.log_entries[0].get("hash"),
|
||||
},
|
||||
"last_entry": {
|
||||
"timestamp": self.log_entries[-1].get("timestamp"),
|
||||
"hash": self.log_entries[-1].get("hash"),
|
||||
},
|
||||
"chain_length": len(self.log_entries),
|
||||
}
|
||||
|
||||
def export_logs(self, output_file: str, include_integrity: bool = True) -> bool:
|
||||
"""
|
||||
Export logs to a file with optional integrity verification.
|
||||
|
||||
Args:
|
||||
output_file: Path to output file
|
||||
include_integrity: Whether to include verification results
|
||||
|
||||
Returns:
|
||||
True if export successful
|
||||
"""
|
||||
try:
|
||||
export_data = {
|
||||
"logs": self.log_entries,
|
||||
"export_timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
if include_integrity:
|
||||
export_data["integrity"] = self.verify_chain()
|
||||
export_data["chain_info"] = self.get_chain_info()
|
||||
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
json.dump(export_data, f, indent=2)
|
||||
|
||||
return True
|
||||
except (IOError, json.JSONEncodeError):
|
||||
return False
|
||||
Reference in New Issue
Block a user