M11 - Email Notifications + Audit Logging Hardening: - Email notifier (lettre crate) with templates for patch failure, job completion, maintenance reminders - Audit log hash chaining (prev_hash + row_hash) for tamper-evident logging - Periodic + on-demand audit integrity verification - Audit logging for all config changes and certificate operations - Frontend: email settings integration, audit integrity verification action M12 - Deployment Packaging, Backup/DR, Integration Testing: - scripts/backup.sh: Nightly pg_dump, CA backup (GPG), config backup (secrets excluded unless encrypted) - scripts/setup.sh: Enhanced with backup dir, seed migration, backup cron, systemd target install - systemd units: Restart=always, WatchdogSec, ReadWritePaths, security hardening - systemd/patch-manager.target: Service target for coordinated lifecycle - docs/runbooks/restore.md: Full DR runbook with RPO 24h / RTO 4h targets - scripts/integration-test.sh: 9 test suites covering full API lifecycle - scripts/performance-test.sh: NFR validation (dashboard <5s, CIDR /22 <10s, API <2s) - docs/security-review.md: Comprehensive security control verification - docs/compliance-mapping.md: HIPAA (6 sections) + PCI-DSS v4.0 (9 requirements) mapped
87 lines
2.5 KiB
Rust
87 lines
2.5 KiB
Rust
//! Periodic audit log integrity verification.
|
|
//!
|
|
//! Runs every 24 hours, walks the audit_log rows ordered by id,
|
|
//! verifies each row_hash matches the recomputed hash, and logs the
|
|
//! result as an `AuditIntegrityVerified` event. If tampering is
|
|
//! detected, logs an error and creates an alert.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use sqlx::PgPool;
|
|
|
|
use pm_core::audit::{log_event, verify_integrity, AuditAction};
|
|
use pm_core::config::AppConfig;
|
|
|
|
/// Run the audit integrity verifier every 24 hours.
|
|
pub async fn run_audit_verifier(pool: PgPool, _config: Arc<AppConfig>) {
|
|
tracing::info!("Audit integrity verifier started");
|
|
|
|
// Run immediately on startup
|
|
verify_once(&pool).await;
|
|
|
|
let mut interval = tokio::time::interval(Duration::from_secs(24 * 60 * 60));
|
|
loop {
|
|
interval.tick().await;
|
|
tracing::info!("Running scheduled audit integrity verification");
|
|
verify_once(&pool).await;
|
|
}
|
|
}
|
|
|
|
/// Run a single integrity verification pass.
|
|
async fn verify_once(pool: &PgPool) {
|
|
let result = verify_integrity(pool).await;
|
|
|
|
if result.intact {
|
|
tracing::info!(
|
|
rows_checked = result.rows_checked,
|
|
"Audit integrity verification passed"
|
|
);
|
|
} else {
|
|
tracing::error!(
|
|
rows_checked = result.rows_checked,
|
|
error_count = result.errors.len(),
|
|
"Audit integrity verification FAILED — tampering detected!"
|
|
);
|
|
|
|
for err in &result.errors {
|
|
tracing::error!(
|
|
row_id = err.row_id,
|
|
expected_hash = %err.expected_hash,
|
|
actual_hash = %err.actual_hash,
|
|
"Audit chain integrity error"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Log the verification event
|
|
log_event(
|
|
pool,
|
|
AuditAction::AuditIntegrityVerified,
|
|
None,
|
|
None,
|
|
Some("audit_log"),
|
|
None,
|
|
serde_json::json!({
|
|
"intact": result.intact,
|
|
"rows_checked": result.rows_checked,
|
|
"error_count": result.errors.len(),
|
|
"errors": result.errors.iter().take(10).map(|e| serde_json::json!({
|
|
"row_id": e.row_id,
|
|
"expected_hash": e.expected_hash,
|
|
"actual_hash": e.actual_hash,
|
|
})).collect::<Vec<_>>(),
|
|
}),
|
|
None,
|
|
None,
|
|
)
|
|
.await;
|
|
|
|
// Update last verified timestamp
|
|
let _ = sqlx::query(
|
|
"UPDATE system_config SET value = NOW()::text, updated_at = NOW() WHERE key = 'audit_integrity_last_verified'",
|
|
)
|
|
.execute(pool)
|
|
.await;
|
|
}
|