Private
Public Access
1
0

feat(M11+M12): Email notifications, audit hardening, deployment packaging, backup/DR, integration testing

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
This commit is contained in:
2026-04-24 00:45:51 +00:00
parent 84ab92f4f0
commit 297bf1bd83
26 changed files with 2651 additions and 65 deletions

View File

@ -22,6 +22,7 @@ use tokio::{sync::Semaphore, time};
use uuid::Uuid;
use crate::agent_loader::load_agent_certs;
use crate::email;
// ─────────────────────────────────────────────────────────────────────────────
// Internal DB row types
@ -710,6 +711,8 @@ async fn handle_host_failure(pool: PgPool, pjh_id: Uuid, error_msg: String) {
/// 2. All hosts `succeeded` → parent `succeeded`.
/// 3. All hosts `cancelled` → parent `cancelled`.
/// 4. Any `failed` with none still active → parent `failed` (includes partial).
///
/// After rolling up, sends email notifications for completed/failed jobs.
async fn sync_job_status(pool: &PgPool, job_id: Uuid) {
let counts: StatusCounts = match sqlx::query_as(
r#"
@ -798,6 +801,57 @@ async fn sync_job_status(pool: &PgPool, job_id: Uuid) {
if let Err(e) = result {
tracing::error!(%job_id, error = %e, "sync_job_status: failed to update parent job");
}
// Send email notifications for completed/failed jobs
if set_completed {
// Spawn email notification in background — non-blocking
let pool_clone = pool.clone();
let job_id_str = job_id.to_string();
let total = counts.total_count;
let succeeded = counts.succeeded_count;
let failed = counts.failed_count;
tokio::spawn(async move {
email::send_job_completion_email(
&pool_clone,
&job_id_str,
total,
succeeded,
failed,
).await;
// If there are failures, also send failure emails per host
if failed > 0 {
let failed_hosts: Vec<(String, String)> = match sqlx::query_as(
r#"
SELECT h.fqdn, COALESCE(pjh.error_message, 'Unknown error')
FROM patch_job_hosts pjh
JOIN hosts h ON h.id = pjh.host_id
WHERE pjh.job_id = $1 AND pjh.status = 'failed'
"#,
)
.bind(job_id)
.fetch_all(&pool_clone)
.await
{
Ok(rows) => rows,
Err(e) => {
tracing::error!(%job_id, error = %e, "sync_job_status: failed to fetch failed hosts for email");
Vec::new()
}
};
for (fqdn, error_msg) in failed_hosts {
email::send_patch_failure_email(
&pool_clone,
&fqdn,
&job_id_str,
&error_msg,
).await;
}
}
});
}
}
// ─────────────────────────────────────────────────────────────────────────────