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:
@ -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 ── */}
|
||||
|
||||
Reference in New Issue
Block a user