OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

verify.php

13 KB
<?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&auml;t aller Eintr&auml;ge zu gew&auml;hrleisten. Jeder Eintrag enth&auml;lt den Hash des vorherigen Eintrags, sodass nachtr&auml;gliche &Auml;nderungen erkannt werden k&ouml;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&uuml;ft werden?') ?></p>
                </div>
                <div class="form-group">
                    <label for="limit"><?= $this->te('Anzahl Eintr&auml;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&auml;ge sollen gepr&uuml;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&auml;tsbericht (Hash-Ketten-Verifikation und Kennzahlen) f&uuml;r einen optionalen Zeitraum &ndash; als PDF f&uuml;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">&#10003;</div>
                    <h2><?= $this->te('Integrit&auml;t best&auml;tigt') ?></h2>
                    <p><?= $this->t('Alle {anzahl} gepr&uuml;ften Eintr&auml;ge sind integer.', ['anzahl' => number_format($result['checked'] ?? 0)]) ?></p>
                <?php else: ?>
                    <div class="result-icon">&#9888;</div>
                    <h2><?= $this->te('Integrit&auml;tsfehler erkannt!') ?></h2>
                    <p>
                        <?= $this->t('Bei {fehlerhaft} von {gesamt} Eintr&auml;gen wurde ein Integrit&auml;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&uuml;fter Bereich') ?></dt>
                    <dd><?= $this->te('Eintrag #{von} bis #{bis}', ['von' => $start_id, 'bis' => $start_id + $limit - 1]) ?></dd>

                    <dt><?= $this->te('Gepr&uuml;fte Eintr&auml;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&auml;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&auml;tsfehler k&ouml;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&uuml;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&auml;ge entstanden, bevor der Anwendungsschl&uuml;ssel (APP_KEY) auf dieser Installation eingerichtet wurde &ndash; sie sind ohne Schl&uuml;ssel signiert und lassen sich mit dem heutigen Schl&uuml;ssel nicht mehr nachrechnen. Ihre Kette ist in sich stimmig, ein Manipulationsverdacht besteht nicht; kryptografisch belegbar sind jedoch erst die Eintr&auml;ge ab der Schl&uuml;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&auml;ge wurden geschrieben, bevor die Pr&uuml;fsumme unabh&auml;ngig von der Speicher-Schreibweise gebildet wurde. Ihre Detailfelder liegen als JSON in der Datenbank, die sie beim Lesen umformatiert zur&uuml;ckgibt &ndash; unter anderem mit anders sortierten Schl&uuml;sseln. Der urspr&uuml;nglich signierte Text existiert damit nicht mehr, weshalb sich die Signatur nicht nachrechnen l&auml;sst. Der Inhalt ist unver&auml;ndert lesbar, ein Manipulationsverdacht besteht nicht; kryptografisch belegbar sind die Eintr&auml;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&auml;ge (keine Manipulation)') ?>
                    </h3>
                    <p style="margin:0 0 0.6rem;font-size:0.85rem;color:var(--amber-800);">
                        <?= $this->te('Diese Eintr&auml;ge tragen eine g&uuml;ltige, mit dem Anwendungsschl&uuml;ssel signierte Pr&uuml;fsumme &ndash; sie verweisen lediglich auf einen wenige Positionen &auml;lteren Vorg&auml;nger. Das entsteht, wenn zwei Vorg&auml;nge gleichzeitig protokollieren (z.&nbsp;B. Hintergrundjob und Benutzeraktion), und stellt keine Ver&auml;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'] ?> &middot; <?= $this->e(date('d.m.Y H:i', strtotime($fork['timestamp'] ?? ''))) ?></a>
                        <?php endforeach; ?>
                    </div>
                </div>
            <?php endif; ?>
        </section>
    <?php endif; ?>

    <!-- Erkl&auml;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&auml;ngers enth&auml;lt, ergibt sich eine Kette. Eine &Auml;nderung an einem Eintrag w&uuml;rde alle nachfolgenden Hashes invalidieren.') ?>
                </li>
                <li>
                    <?= $this->t('<strong>Verifizierung:</strong> Bei der Pr&uuml;fung wird f&uuml;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">&darr;</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">&darr;</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>