Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', [
'page_title' => 'Hash-Ketten-Verifizierung',
'current_admin_page' => 'audit'
]); ?>
<div class="verify-page">
<header class="page-header">
<p class="page-description">
<?= $this->te('Das Audit-Log verwendet eine kryptographische Hash-Kette (SHA-256), um die Integrität aller Einträge zu gewährleisten. Jeder Eintrag enthält den Hash des vorherigen Eintrags, sodass nachträgliche Änderungen erkannt werden können.') ?>
</p>
</header>
<!-- Verifizierungsformular -->
<section class="verify-form-section">
<form action="/admin/audit/verify" method="GET" class="verify-form">
<div class="form-row">
<div class="form-group">
<label for="start_id"><?= $this->te('Start-ID') ?></label>
<input type="number" id="start_id" name="start_id" class="form-control"
value="<?= $start_id ?>" min="1">
<p class="form-hint"><?= $this->te('Ab welchem Eintrag soll geprüft werden?') ?></p>
</div>
<div class="form-group">
<label for="limit"><?= $this->te('Anzahl Einträge') ?></label>
<input type="number" id="limit" name="limit" class="form-control"
value="<?= $limit ?>" min="1" max="1000">
<p class="form-hint"><?= $this->te('Wie viele Einträge sollen geprüft werden? (max. 1000)') ?></p>
</div>
<div class="verify-actions">
<button type="submit" class="btn btn-primary"><?= $this->te('Verifizieren') ?></button>
</div>
</div>
</form>
</section>
<!-- Prüfbericht herunterladen -->
<section class="verify-form-section">
<h3><?= $this->te('Revisionssicherer Prüfbericht') ?></h3>
<p class="page-description">
<?= $this->te('Erzeugt einen Integritätsbericht (Hash-Ketten-Verifikation und Kennzahlen) für einen optionalen Zeitraum – als PDF für die Revision oder als CSV zur Weiterverarbeitung. Leer lassen = gesamter Zeitraum.') ?>
</p>
<form action="/admin/audit/integrity-report" method="GET" class="verify-form">
<div class="form-row">
<div class="form-group">
<label for="ir_from"><?= $this->te('Von (optional)') ?></label>
<input type="date" id="ir_from" name="date_from" class="form-control">
</div>
<div class="form-group">
<label for="ir_to"><?= $this->te('Bis (optional)') ?></label>
<input type="date" id="ir_to" name="date_to" class="form-control">
</div>
<div class="verify-actions">
<button type="submit" name="format" value="pdf" class="btn btn-primary">
<i class="fas fa-file-pdf mr-1"></i>PDF
</button>
<button type="submit" name="format" value="csv" class="btn btn-outline-secondary">
<i class="fas fa-file-csv mr-1"></i>CSV
</button>
</div>
</div>
</form>
</section>
<!-- Ergebnis -->
<?php if ($result): ?>
<section class="verify-result">
<div class="result-summary <?= $result['valid'] ? 'result-success' : 'result-error' ?>">
<?php if ($result['valid']): ?>
<div class="result-icon">✓</div>
<h2><?= $this->te('Integrität bestätigt') ?></h2>
<p><?= $this->t('Alle {anzahl} geprüften Einträge sind integer.', ['anzahl' => number_format($result['checked'] ?? 0)]) ?></p>
<?php else: ?>
<div class="result-icon">⚠</div>
<h2><?= $this->te('Integritätsfehler erkannt!') ?></h2>
<p>
<?= $this->t('Bei {fehlerhaft} von {gesamt} Einträgen wurde ein Integritätsfehler festgestellt.', ['fehlerhaft' => number_format(count($result['invalid'] ?? [])), 'gesamt' => number_format($result['checked'] ?? 0)]) ?>
</p>
<?php endif; ?>
</div>
<div class="result-details">
<h3><?= $this->te('Details') ?></h3>
<dl class="detail-list">
<dt><?= $this->te('Geprüfter Bereich') ?></dt>
<dd><?= $this->te('Eintrag #{von} bis #{bis}', ['von' => $start_id, 'bis' => $start_id + $limit - 1]) ?></dd>
<dt><?= $this->te('Geprüfte Einträge') ?></dt>
<dd><?= number_format($result['checked'] ?? 0) ?></dd>
<dt><?= $this->te('Gesamt im Log') ?></dt>
<dd><?= number_format($result['total'] ?? 0) ?></dd>
</dl>
</div>
<?php if (!$result['valid'] && !empty($result['invalid'])): ?>
<div class="result-errors">
<h3><?= $this->te('Fehlerhafte Einträge') ?></h3>
<table class="data-table">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col"><?= $this->te('Zeitstempel') ?></th>
<th scope="col"><?= $this->te('Erwarteter Hash') ?></th>
<th scope="col"><?= $this->te('Gespeicherter Hash') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($result['invalid'] as $error): ?>
<tr>
<td>
<a href="/admin/audit/<?= $error['id'] ?>">#<?= $error['id'] ?></a>
</td>
<td><?= date('d.m.Y H:i:s', strtotime($error['timestamp'] ?? '')) ?></td>
<td><code class="hash-short"><?= substr($error['expected_checksum'] ?? '', 0, 16) ?>...</code></td>
<td><code class="hash-short"><?= substr($error['stored_checksum'] ?? '', 0, 16) ?>...</code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class="error-note">
<?= $this->t('<strong>Wichtig:</strong> Integritätsfehler können auf unbefugte Manipulationen des Audit-Logs hindeuten. Diese Situation sollte umgehend untersucht werden.') ?>
</p>
</div>
<?php endif; ?>
<?php if (!empty($result['pre_key'])): ?>
<div style="margin-top:1rem;background:var(--gray-50);border:1px solid var(--gray-200);border-radius:var(--radius);padding:.85rem 1rem;">
<h3 style="margin:0 0 .4rem;font-size:.95rem;color:var(--gray-700);">
<i class="fas fa-key"></i> <?= $this->t('Vor Einrichtung der Verschlüsselung geschrieben ({anzahl})', ['anzahl' => number_format(count($result['pre_key']))]) ?>
</h3>
<p style="margin:0;font-size:.85rem;color:var(--gray-700);">
<?= $this->t('Diese Einträge entstanden, bevor der Anwendungsschlüssel (APP_KEY) auf dieser Installation eingerichtet wurde – sie sind ohne Schlüssel signiert und lassen sich mit dem heutigen Schlüssel nicht mehr nachrechnen. Ihre Kette ist in sich stimmig, ein Manipulationsverdacht besteht nicht; kryptografisch belegbar sind jedoch erst die Einträge ab der Schlüssel-Einrichtung. Bereich: #{von} bis #{bis}.', ['von' => (int)$result['pre_key'][0]['id'], 'bis' => (int)$result['pre_key'][count($result['pre_key']) - 1]['id']]) ?>
</p>
</div>
<?php endif; ?>
<?php if (!empty($result['storage_format'])): ?>
<div style="margin-top:1rem;background:var(--gray-50);border:1px solid var(--gray-200);border-radius:var(--radius);padding:.85rem 1rem;">
<h3 style="margin:0 0 .4rem;font-size:.95rem;color:var(--gray-700);">
<i class="fas fa-database"></i> <?= $this->t('Von der Datenbank umformatiert gespeichert ({anzahl})', ['anzahl' => number_format(count($result['storage_format']))]) ?>
</h3>
<p style="margin:0;font-size:.85rem;color:var(--gray-700);">
<?= $this->t('Diese Einträge wurden geschrieben, bevor die Prüfsumme unabhängig von der Speicher-Schreibweise gebildet wurde. Ihre Detailfelder liegen als JSON in der Datenbank, die sie beim Lesen umformatiert zurückgibt – unter anderem mit anders sortierten Schlüsseln. Der ursprünglich signierte Text existiert damit nicht mehr, weshalb sich die Signatur nicht nachrechnen lässt. Der Inhalt ist unverändert lesbar, ein Manipulationsverdacht besteht nicht; kryptografisch belegbar sind die Einträge ab dieser Umstellung. Bereich: #{von} bis #{bis}.', ['von' => (int)$result['storage_format'][0]['id'], 'bis' => (int)$result['storage_format'][count($result['storage_format']) - 1]['id']]) ?>
</p>
</div>
<?php endif; ?>
<?php if (!empty($result['forks'])): ?>
<div style="margin-top:1rem;background:var(--amber-50);border:1px solid var(--amber-200);border-radius:var(--radius);padding:0.85rem 1rem;">
<h3 style="margin:0 0 0.4rem;font-size:0.95rem;color:var(--amber-800);">
<i class="fas fa-code-branch"></i> <?= $this->te('Parallel geschriebene Einträge (keine Manipulation)') ?>
</h3>
<p style="margin:0 0 0.6rem;font-size:0.85rem;color:var(--amber-800);">
<?= $this->te('Diese Einträge tragen eine gültige, mit dem Anwendungsschlüssel signierte Prüfsumme – sie verweisen lediglich auf einen wenige Positionen älteren Vorgänger. Das entsteht, wenn zwei Vorgänge gleichzeitig protokollieren (z. B. Hintergrundjob und Benutzeraktion), und stellt keine Veränderung der Inhalte dar.') ?>
</p>
<div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<?php foreach ($result['forks'] as $fork): ?>
<a href="/admin/audit/<?= (int)$fork['id'] ?>" class="badge badge-warning" style="text-decoration:none;">#<?= (int)$fork['id'] ?> · <?= $this->e(date('d.m.Y H:i', strtotime($fork['timestamp'] ?? ''))) ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
<!-- Erklärung -->
<section class="verify-explanation">
<h3><?= $this->te('Wie funktioniert die Hash-Kette?') ?></h3>
<div class="explanation-content">
<ol>
<li>
<?= $this->t('<strong>Eintrag erstellen:</strong> Wenn ein Audit-Eintrag geschrieben wird, werden alle relevanten Daten zusammen mit dem Hash des vorherigen Eintrags zu einem String kombiniert.') ?>
</li>
<li>
<?= $this->t('<strong>Hash berechnen:</strong> Aus diesem String wird ein SHA-256 Hash berechnet und zusammen mit dem Eintrag gespeichert.') ?>
</li>
<li>
<?= $this->t('<strong>Verkettung:</strong> Da jeder Eintrag den Hash seines Vorgängers enthält, ergibt sich eine Kette. Eine Änderung an einem Eintrag würde alle nachfolgenden Hashes invalidieren.') ?>
</li>
<li>
<?= $this->t('<strong>Verifizierung:</strong> Bei der Prüfung wird für jeden Eintrag der Hash neu berechnet und mit dem gespeicherten verglichen.') ?>
</li>
</ol>
<div class="hash-diagram">
<div class="hash-entry">
<span class="entry-label"><?= $this->te('Eintrag N-1') ?></span>
<span class="entry-hash"><?= $this->te('Hash: abc123...') ?></span>
</div>
<div class="hash-arrow">↓</div>
<div class="hash-entry">
<span class="entry-label"><?= $this->te('Eintrag N') ?></span>
<span class="entry-data"><?= $this->te('Daten + "abc123..."') ?></span>
<span class="entry-hash"><?= $this->te('Hash: def456...') ?></span>
</div>
<div class="hash-arrow">↓</div>
<div class="hash-entry">
<span class="entry-label"><?= $this->te('Eintrag N+1') ?></span>
<span class="entry-data"><?= $this->te('Daten + "def456..."') ?></span>
<span class="entry-hash"><?= $this->te('Hash: ghi789...') ?></span>
</div>
</div>
</div>
</section>
</div>