OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

AuditCoverageTest.php

9 KB
<?php

declare(strict_types=1);

/**
 * Haelt fest, welche Schreibwege protokolliert werden – und welche bewusst nicht.
 *
 * Ein Protokoll nuetzt nur, solange es vollstaendig ist: Faellt ein Eintrag
 * weg, merkt das niemand, denn es fehlt ja gerade die Spur. Umgekehrt ist ein
 * Protokoll wertlos, wenn jeder Tastendruck darin steht – dann findet niemand
 * mehr die eine Aenderung, auf die es ankommt.
 *
 * Deshalb halten diese Faelle beide Richtungen fest: geschlossene Luecken
 * bleiben geschlossen, und die bewussten Ausnahmen bleiben Ausnahmen.
 */
return static function (): void {

    $wurzel = dirname(__DIR__);
    $lies   = static fn (string $pfad): string => (string)file_get_contents($wurzel . '/src/Controllers/' . $pfad);

    /**
     * Sucht den Rumpf einer Methode.
     *
     * @return string Rumpf bis zur naechsten Methode gleicher Einrueckung.
     */
    $rumpf = static function (string $quelle, string $methode): string {
        $start = strpos($quelle, 'function ' . $methode . '(');
        if ($start === false) {
            return '';
        }
        $naechste = preg_match('/\n    (?:public|private|protected)\s+function\s/', substr($quelle, $start), $m, PREG_OFFSET_CAPTURE)
            ? $m[0][1] : strlen($quelle) - $start;
        return substr($quelle, $start, $naechste);
    };

    /** @return bool Protokolliert der Rumpf? */
    $loggt = static fn (string $body): bool => (bool)preg_match('/audit\(\)->log|auditLogger|getAuditLogger\(\)/', $body);

    // ----------------------------------------------------------------
    // Geschlossene Luecken
    // ----------------------------------------------------------------

    test('Loeschungen in der Verwaltung hinterlassen eine Spur', function () use ($lies, $rumpf, $loggt) {
        // Was geloescht wurde, laesst sich nachher nirgends mehr ablesen –
        // ausser im Protokoll.
        // Schlagworte und Seitenvorlagen protokolliert ihre Fachklasse – siehe
        // den Fall weiter unten; hier stehen die Wege ohne eigene Fachklasse.
        $faelle = [
            'Admin/AssetAdminController.php'    => ['typeImagesDestroy', 'categoryFieldsDestroy', 'typeFieldsDestroy'],
            'Admin/WorkflowAdminController.php' => ['deleteField', 'deleteStep'],
            'WorkflowController.php'            => ['deleteWebhook'],
        ];
        foreach ($faelle as $datei => $methoden) {
            $quelle = $lies($datei);
            foreach ($methoden as $methode) {
                $body = $rumpf($quelle, $methode);
                assertTrue($body !== '', "Methode {$methode} in {$datei} nicht gefunden");
                assertTrue($loggt($body), "Loeschung ohne Protokolleintrag: {$datei}::{$methode}");
            }
        }
    });

    test('Eine Loeschung liest den Datensatz, bevor sie ihn entfernt', function () use ($lies, $rumpf) {
        // Sonst steht im Protokoll nur eine Nummer und niemand weiss, was fehlt.
        $faelle = [
            'Admin/WorkflowAdminController.php' => 'deleteField',
            'WorkflowController.php'            => 'deleteWebhook',
        ];
        foreach ($faelle as $datei => $methode) {
            $body   = $rumpf($lies($datei), $methode);
            $lesen  = (int)preg_match('/fetchOne|findById|ownWebhook/', $body, $m, PREG_OFFSET_CAPTURE) ? $m[0][1] : PHP_INT_MAX;
            $loesch = preg_match('/->delete\(|DELETE FROM/', $body, $m2, PREG_OFFSET_CAPTURE) ? $m2[0][1] : PHP_INT_MAX;
            assertTrue($lesen < $loesch, "Wird erst geloescht, dann gelesen: {$datei}::{$methode}");
        }
    });

    test('Aenderungen an der Einrichtung stehen im Protokoll', function () use ($lies, $rumpf, $loggt) {
        $faelle = [
            'Admin/AssetPdfTemplateController.php' => ['update'],
            'Admin/MonitoringAdminController.php' => ['saveSettings', 'regenerateToken'],
            'Admin/WordPressAdminController.php'  => ['saveTexts'],
            'Admin/DirectoryAdminController.php'  => ['uploadImage'],
            'Admin/AssetAdminController.php'      => ['typeImagesUpload'],
        ];
        foreach ($faelle as $datei => $methoden) {
            $quelle = $lies($datei);
            foreach ($methoden as $methode) {
                $body = $rumpf($quelle, $methode);
                assertTrue($body !== '', "Methode {$methode} in {$datei} nicht gefunden");
                assertTrue($loggt($body), "Einrichtung geaendert ohne Protokolleintrag: {$datei}::{$methode}");
            }
        }
    });

    test('Das Portal protokolliert wie der Weg im Haus', function () use ($lies, $rumpf) {
        // Dieselbe Aenderung darf nicht davon abhaengen, ueber welchen Zugang
        // sie hereinkam – sonst entgeht dem Protokoll alles aus dem Portal.
        $quelle = $lies('WordPressApiController.php');
        foreach (['createOrder', 'ticketReply', 'cancelLoanRequest'] as $methode) {
            $body = $rumpf($quelle, $methode);
            assertTrue($body !== '', "Methode {$methode} nicht gefunden");
            assertTrue(str_contains($body, '$this->protokolliere('), "Portal-Aktion ohne Protokolleintrag: {$methode}");
        }
    });

    test('Ein Protokollfehler laesst die Portal-Antwort unberuehrt', function () use ($wurzel, $rumpf) {
        // Das Portal wartet auf eine Antwort; ein Protokollfehler darf den
        // Vorgang nicht scheitern lassen, nachdem er bereits geschrieben wurde.
        $body = $rumpf((string)file_get_contents($wurzel . '/src/Api/TokenAuth.php'), 'protokolliere');
        assertTrue(str_contains($body, 'try {'), 'Der Protokolleintrag ist nicht abgesichert');
        assertTrue(str_contains($body, 'catch (\Throwable)'), 'Ein Protokollfehler schlaegt durch');
    });

    test('Wo die Fachklasse protokolliert, tut es der Controller nicht noch einmal', function () use ($wurzel, $lies, $rumpf, $loggt) {
        // Ein zweiter Eintrag fuer denselben Vorgang macht das Protokoll nicht
        // vollstaendiger, nur laenger – und laesst eine Aenderung doppelt
        // erscheinen, als waere sie zweimal geschehen.
        $paare = [
            'Admin/TagController.php'          => ['Content/TagManager.php', ['create_tag', 'update_tag', 'delete_tag']],
            'Admin/PageTemplateController.php' => ['Content/PageTemplateManager.php', ['create_template', 'update_template', 'delete_template']],
        ];
        foreach ($paare as $controller => [$manager, $vorgaenge]) {
            $quelleM = (string)file_get_contents($wurzel . '/src/' . $manager);
            foreach ($vorgaenge as $vorgang) {
                assertTrue(str_contains($quelleM, "'{$vorgang}'"), "{$manager} protokolliert {$vorgang} nicht mehr");
            }
            $quelleC = $lies($controller);
            foreach (['store', 'update', 'destroy'] as $methode) {
                $body = $rumpf($quelleC, $methode);
                if ($body === '') { continue; }
                assertFalse($loggt($body), "Doppelter Protokolleintrag: {$controller}::{$methode} neben {$manager}");
            }
        }
    });

    // ----------------------------------------------------------------
    // Bewusste Ausnahmen
    // ----------------------------------------------------------------

    test('Was staendig passiert, bleibt aus dem Protokoll', function () use ($lies, $rumpf, $loggt) {
        // Diese Wege schreiben im Sekundentakt oder ohne Zutun eines Menschen.
        // Ein Eintrag je Vorgang machte das Protokoll unlesbar – die eigentliche
        // Aenderung steht jeweils an anderer Stelle darin.
        $ausnahmen = [
            'TicketController.php'    => ['viewersPing'],      // wer gerade zusieht
            'PageController.php'      => ['autosave'],         // Entwurf im Sekundentakt
            'MonitoringController.php' => ['webhook'],         // Meldungen der Überwachung
            'WordPressApiController.php' => ['preflight'],     // technischer Handschlag
        ];
        foreach ($ausnahmen as $datei => $methoden) {
            $quelle = $lies($datei);
            foreach ($methoden as $methode) {
                $body = $rumpf($quelle, $methode);
                assertTrue($body !== '', "Methode {$methode} in {$datei} nicht gefunden");
                assertFalse($loggt($body), "Protokollflut droht: {$datei}::{$methode} schreibt jetzt Eintraege");
            }
        }
    });

    test('Sperren und Anwesenheit bleiben aussen vor', function () use ($lies, $rumpf, $loggt) {
        // Eine Schreibsperre sagt nichts darueber, ob sich etwas geaendert hat;
        // das Speichern selbst steht im Protokoll.
        $quelle = $lies('PageController.php');
        foreach (['writeLock', 'deleteLock'] as $methode) {
            assertFalse($loggt($rumpf($quelle, $methode)), "Sperre erzeugt jetzt Protokolleintraege: {$methode}");
        }
    });
};