feat(PROJ-48): Audit-Log Unveränderbarkeit (Trigger, append-only Logfile, Healthcheck)
DB-Trigger audit_log_immutable verhindert UPDATE/DELETE auf audit_log, zusätzliches append-only JSON-Lines-Logfile (audit.log_path) als tamper-evident Backup, neuer Healthcheck-Prüfpunkt in archivmail status. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ func runStatus(args []string) {
|
||||
checkPostgres(cfg),
|
||||
checkManticore(cfg),
|
||||
checkStorage(cfg),
|
||||
checkAuditLog(cfg),
|
||||
}
|
||||
|
||||
allOK := true
|
||||
@@ -172,6 +173,28 @@ func checkStorage(cfg *config.Config) checkResult {
|
||||
return checkResult{Name: "Storage", OK: ok, Detail: detail}
|
||||
}
|
||||
|
||||
// checkAuditLog verifies that the append-only audit log file (PROJ-48) exists
|
||||
// and is writable. It performs a non-destructive O_APPEND open without writing
|
||||
// any bytes, so the immutability of existing content is not affected.
|
||||
func checkAuditLog(cfg *config.Config) checkResult {
|
||||
path := cfg.Audit.ResolvedLogPath()
|
||||
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0o640)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return checkResult{Name: "Audit-Log", OK: false, Detail: fmt.Sprintf("%s existiert nicht", path)}
|
||||
}
|
||||
return checkResult{Name: "Audit-Log", OK: false, Detail: fmt.Sprintf("%s nicht beschreibbar: %v", path, err)}
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
detail := path + ", beschreibbar"
|
||||
if fi, statErr := f.Stat(); statErr == nil {
|
||||
detail = fmt.Sprintf("%s, beschreibbar, %s", path, formatBytes(uint64(fi.Size())))
|
||||
}
|
||||
return checkResult{Name: "Audit-Log", OK: true, Detail: detail}
|
||||
}
|
||||
|
||||
func formatBytes(b uint64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
|
||||
@@ -248,7 +248,7 @@ func main() {
|
||||
defer users.Close()
|
||||
|
||||
// Audit log
|
||||
audlog, err := audit.New(cfg.Database.DSN(), cfg.Audit.LogPath, logger)
|
||||
audlog, err := audit.New(cfg.Database.DSN(), cfg.Audit.ResolvedLogPath(), logger)
|
||||
if err != nil {
|
||||
logger.Error("audit init failed", "err", err)
|
||||
os.Exit(1)
|
||||
|
||||
Reference in New Issue
Block a user