Files
sshkeyvault/tests/protokoll-aktionen.test.ts
florian.wildeandClaude Opus 5.5 0154343fd2 feat(protokoll): Audit-Log mit Filtern, Kettenpruefung und CSV (#027)
Das Protokoll war bisher nur in der Datenbank lesbar. Die Ansicht zeigt es
jeder Rolle mit lesbaren Aktionstexten, aufklappbarem Diff, Details und
Hashes; ein Test haelt fest, dass jede Aktion im Code einen Text hat.

Geblaettert wird ueber die ID statt OFFSET, weil das Protokoll waehrend
des Lesens waechst. Die Kettenpruefung liest jede Zeile und laeuft deshalb
nur auf Knopfdruck, mit 5 Minuten Zwischenspeicher; ein Bruch wird selbst
protokolliert. Der CSV-Export streamt in Stapeln, entschaerft Formeln und
steht selbst im Protokoll. protokollieren() reicht jeden Eintrag nach dem
Schreiben an Syslog weiter, ohne Details und Diff.

Kein Weg aendert oder loescht Eintraege; ein struktureller Test sichert
das ab, zusaetzlich zum Trigger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:47:44 +02:00

76 lines
3.2 KiB
TypeScript

import { readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { AKTIONEN, AKTIONSGRUPPEN, aktionenDerGruppe, aktionText, aktionTon } from "@/lib/protokoll-aktionen";
/**
* Jede Protokoll-Aktion im Quellcode hat einen lesbaren Text (#027). Gesucht wird in allen
* Dateien, die `protokollieren(` rufen, nach `aktion: …`/`aktion = …` (auch in Ternaries), und
* ueberall nach Literalen, die mit einem bekannten Praefix beginnen (`"schluessel.uebernommen"`
* wird z. B. als Argument durchgereicht).
*/
function dateien(verzeichnis: string): string[] {
return readdirSync(verzeichnis).flatMap((name) => {
const pfad = path.join(verzeichnis, name);
if (statSync(pfad).isDirectory()) return name === "generated" ? [] : dateien(pfad);
return /\.tsx?$/.test(name) ? [pfad] : [];
});
}
const QUELLEN = dateien("src").map((datei) => ({ datei, inhalt: readFileSync(datei, "utf8") }));
const LITERAL = /"([^"\n]+)"/g;
function aktionenImCode(): Map<string, string> {
const gefunden = new Map<string, string>();
const praefixe = new Set(Object.keys(AKTIONEN).map((a) => a.split(".")[0]!));
for (const { datei, inhalt } of QUELLEN) {
if (inhalt.includes("protokollieren(")) {
for (const treffer of inhalt.matchAll(/\baktion\s*[:=]\s*([^,\n]+)/g)) {
// Bei `a === "x" ? "eins" : "zwei"` zaehlen nur die Zweige, nicht die Bedingung.
const ausdruck = treffer[1]!.includes("?")
? treffer[1]!.slice(treffer[1]!.indexOf("?"))
: treffer[1]!;
for (const [, literal] of ausdruck.matchAll(LITERAL)) gefunden.set(literal!, datei);
}
}
for (const [, literal] of inhalt.matchAll(/"([a-z_]+(?:\.[a-z_]+)+)"/g)) {
if (praefixe.has(literal!.split(".")[0]!) && !literal!.endsWith(".ts")) gefunden.set(literal!, datei);
}
}
return gefunden;
}
describe("Protokoll-Aktionen", () => {
const imCode = aktionenImCode();
it("findet die Aktionen im Quellcode", () => {
expect(imCode.size).toBeGreaterThan(40);
expect(imCode.has("authorized_keys.geschrieben")).toBe(true);
expect(imCode.has("hostkey.ersetzt")).toBe(true);
expect(imCode.has("privatekey.angezeigt")).toBe(true);
expect(imCode.has("schluessel.uebernommen")).toBe(true);
});
it.each([...imCode])("%s hat einen lesbaren Text (%s)", (aktion) => {
expect(AKTIONEN[aktion], `${aktion} fehlt in src/lib/protokoll-aktionen.ts`).toBeDefined();
});
it("jede Gruppe hat Aktionen", () => {
for (const gruppe of Object.keys(AKTIONSGRUPPEN) as (keyof typeof AKTIONSGRUPPEN)[]) {
expect(aktionenDerGruppe(gruppe).length, gruppe).toBeGreaterThan(0);
}
});
it("faellt fuer unbekannte Aktionen auf den technischen Namen zurueck", () => {
expect(aktionText("hostkey.ersetzt")).toBe("Host-Key ersetzt");
expect(aktionText("gibt.es.nicht")).toBe("gibt.es.nicht");
});
it("faerbt nach Ergebnis und Art", () => {
expect(aktionTon("authorized_keys.geschrieben", "ERFOLG")).toBe("ok");
expect(aktionTon("anmeldung.oidc", "ERFOLG")).toBe("neutral");
expect(aktionTon("anmeldung.verweigert", "VERWEIGERT")).toBe("gefahr");
expect(aktionTon("verbindungstest", "FEHLER")).toBe("gefahr");
});
});