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

@ -21,8 +21,9 @@ import {
} from '@mui/material'
import DescriptionIcon from '@mui/icons-material/Description'
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'
import { reportsApi } from '../api/client'
import type { ReportType, ReportFormat } from '../types'
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser'
import { reportsApi, settingsApi } from '../api/client'
import type { ReportType, ReportFormat, AuditIntegrityResult } from '../types'
// ── Report metadata ───────────────────────────────────────────────────────────
@ -98,6 +99,8 @@ export default function ReportsPage() {
const [groupId, setGroupId] = useState<string>('')
const [downloading, setDownloading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [verifyingIntegrity, setVerifyingIntegrity] = useState(false)
const [integrityResult, setIntegrityResult] = useState<AuditIntegrityResult | null>(null)
const info = REPORT_INFO[reportType]
@ -130,6 +133,20 @@ export default function ReportsPage() {
}
}
const handleVerifyIntegrity = async () => {
setVerifyingIntegrity(true)
setIntegrityResult(null)
try {
const { data } = await settingsApi.auditIntegrity()
setIntegrityResult(data)
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Verification failed'
setIntegrityResult({ intact: false, rows_checked: 0, errors: [{ row_id: 0, expected_hash: '', actual_hash: msg }] })
} finally {
setVerifyingIntegrity(false)
}
}
return (
<Container maxWidth="xl" sx={{ mt: 3 }}>
{/* ── Page header ── */}
@ -224,6 +241,44 @@ export default function ReportsPage() {
</Button>
</Box>
</Paper>
{/* ── Audit Integrity card ── */}
<Paper variant="outlined" sx={{ p: 3, mt: 3 }}>
<Typography variant="subtitle1" fontWeight={600} sx={{ mb: 1 }}>
Audit Integrity Verification
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Verify the audit log hash chain has not been tampered with. Each entry is cryptographically linked to the previous one.
</Typography>
<Button
variant="outlined"
fullWidth
startIcon={verifyingIntegrity ? <CircularProgress size={20} /> : <VerifiedUserIcon />}
onClick={handleVerifyIntegrity}
disabled={verifyingIntegrity}
>
Verify Integrity
</Button>
{integrityResult && (
<Alert severity={integrityResult.intact ? 'success' : 'error'} sx={{ mt: 2 }}>
{integrityResult.intact
? `✓ Chain intact — ${integrityResult.rows_checked} rows verified`
: `✗ Chain compromised! ${integrityResult.errors.length} error(s) in ${integrityResult.rows_checked} rows`}
{integrityResult.errors.length > 0 && (
<Box sx={{ mt: 1 }}>
{integrityResult.errors.slice(0, 5).map((e, i) => (
<Typography key={i} variant="body2">
Row {e.row_id}: expected {e.expected_hash.substring(0, 16)} got {e.actual_hash.substring(0, 16)}
</Typography>
))}
{integrityResult.errors.length > 5 && (
<Typography variant="body2">and {integrityResult.errors.length - 5} more</Typography>
)}
</Box>
)}
</Alert>
)}
</Paper>
</Grid>
{/* ── Info card ── */}