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>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Lesbare Beschriftung der technischen Protokoll-Aktionen (#027).
|
||||
*
|
||||
* Das Protokoll speichert stabile Schluessel wie `authorized_keys.geschrieben`, damit sich
|
||||
* Auswertungen (CSV, Syslog, SIEM) nicht an Oberflaechentexten aufhaengen. Die Ansicht
|
||||
* uebersetzt sie hier. `tests/protokoll-aktionen.test.ts` sucht jede `aktion: "…"` im
|
||||
* Quellcode und verlangt einen Eintrag – eine neue Aktion ohne Text faellt so sofort auf.
|
||||
*
|
||||
* `aendert`: Die Aktion veraendert etwas (Datei geschrieben, Schluessel erzeugt …) und wird bei
|
||||
* Erfolg in --ok gezeigt. Lesende oder rein informative Aktionen (Anmeldung, Abruf, Export)
|
||||
* bleiben neutral, damit das Grün die Stellen markiert, an denen sich etwas getan hat.
|
||||
*/
|
||||
|
||||
export const AKTIONSGRUPPEN = {
|
||||
anmeldung: "Anmeldung",
|
||||
hostkey: "Host-Key",
|
||||
ausrollen: "Ausrollen",
|
||||
schluessel: "Schlüssel",
|
||||
widerruf: "Widerruf/Rotation",
|
||||
zielkonto: "Zielkonto",
|
||||
abgleich: "Abgleich",
|
||||
hosts: "Hosts",
|
||||
stammdaten: "Personen und Vorlagen",
|
||||
protokoll: "Protokoll",
|
||||
} as const;
|
||||
|
||||
export type Aktionsgruppe = keyof typeof AKTIONSGRUPPEN;
|
||||
|
||||
type Beschreibung = { readonly text: string; readonly gruppe: Aktionsgruppe; readonly aendert: boolean };
|
||||
|
||||
const a = (text: string, gruppe: Aktionsgruppe, aendert = true): Beschreibung => ({ text, gruppe, aendert });
|
||||
|
||||
export const AKTIONEN: Readonly<Record<string, Beschreibung>> = {
|
||||
// Anmeldung und Bestaetigung (#004)
|
||||
"anmeldung.oidc": a("Anmeldung (OIDC)", "anmeldung", false),
|
||||
"anmeldung.oidc.erstmals": a("Erste Anmeldung (OIDC)", "anmeldung"),
|
||||
"anmeldung.verweigert": a("Anmeldung verweigert", "anmeldung", false),
|
||||
"anmeldung.notfall": a("Anmeldung Notfall-Admin", "anmeldung", false),
|
||||
"anmeldung.notfall.fehlgeschlagen": a("Notfall-Anmeldung fehlgeschlagen", "anmeldung", false),
|
||||
"anmeldung.notfall.gesperrt": a("Notfall-Anmeldung gesperrt", "anmeldung", false),
|
||||
"anmeldung.notfall.totp_falsch": a("Notfall-Anmeldung: TOTP falsch", "anmeldung", false),
|
||||
"bestaetigung.oidc": a("Anmeldung bestätigt (Re-Auth)", "anmeldung", false),
|
||||
"notfall.totp_eingerichtet": a("TOTP für Notfall-Admin eingerichtet", "anmeldung"),
|
||||
|
||||
// Host-Keys (#009)
|
||||
"hostkey.abgerufen": a("Host-Key abgerufen", "hostkey", false),
|
||||
"hostkey.bestaetigt": a("Host-Key bestätigt", "hostkey"),
|
||||
"hostkey.ersetzt": a("Host-Key ersetzt", "hostkey"),
|
||||
"hostkey.abgelehnt": a("Host-Key abgelehnt", "hostkey"),
|
||||
"hostkey.abweichung": a("Host-Key weicht ab – Verbindung abgebrochen", "hostkey", false),
|
||||
|
||||
// Ausrollen (#016, #017)
|
||||
"authorized_keys.geschrieben": a("authorized_keys geschrieben", "ausrollen"),
|
||||
"privatekey.ausgerollt": a("Private Key ausgerollt", "ausrollen"),
|
||||
"verwendung.public.angelegt": a("Public Key zugewiesen", "ausrollen"),
|
||||
"verwendung.public.entfernen": a("Public-Key-Zuweisung zum Entfernen vorgemerkt", "ausrollen"),
|
||||
"verwendung.private.angelegt": a("Private Key zugewiesen", "ausrollen"),
|
||||
"verwendung.private.entfernen": a("Private-Key-Zuweisung zum Entfernen vorgemerkt", "ausrollen"),
|
||||
|
||||
// Schluessel (#012, #013, #014)
|
||||
"schluessel.erzeugt": a("Schlüssel erzeugt", "schluessel"),
|
||||
"schluessel.importiert": a("Schlüssel importiert", "schluessel"),
|
||||
"schluessel.uebernommen": a("Schlüssel vom Host übernommen", "schluessel"),
|
||||
"schluessel.selbst_hinterlegt": a("Eigenen Public Key hinterlegt", "schluessel"),
|
||||
"schluessel.selbst_entfernt": a("Eigenen Public Key entfernt", "schluessel"),
|
||||
"schluessel.geaendert": a("Schlüssel geändert", "schluessel"),
|
||||
"privatekey.angefordert": a("Private Key angefordert", "schluessel", false),
|
||||
"privatekey.angezeigt": a("Private Key angezeigt", "schluessel", false),
|
||||
"privatekey.heruntergeladen": a("Private Key heruntergeladen", "schluessel", false),
|
||||
"privatekey.geloescht": a("Private Key gelöscht", "schluessel"),
|
||||
"deploymentkey.erzeugt": a("Deployment-Key erzeugt", "schluessel"),
|
||||
|
||||
// Widerruf und Rotation (#023, #024)
|
||||
"schluessel.widerruf.eingeleitet": a("Widerruf eingeleitet", "widerruf"),
|
||||
"schluessel.widerrufen": a("Schlüssel widerrufen", "widerruf"),
|
||||
"schluessel.rotiert": a("Schlüssel rotiert", "widerruf"),
|
||||
"schluessel.rotation.uebernommen": a("Rotation: Zuweisungen übernommen", "widerruf"),
|
||||
|
||||
// Zielkonten (#018)
|
||||
"zielkonto.angelegt": a("Zielkonto angelegt", "zielkonto"),
|
||||
"zielkonto.geaendert": a("Zielkonto geändert", "zielkonto"),
|
||||
"zielkonto.entfernt": a("Zielkonto entfernt", "zielkonto"),
|
||||
|
||||
// Abgleich und Uebernahme (#021, #022)
|
||||
abgleich: a("Abgleich durchgeführt", "abgleich", false),
|
||||
"uebernahme.verwalten": a("Fremden Key übernommen und verwaltet", "abgleich"),
|
||||
"uebernahme.bekannt": a("Fremden Key als bekannt aufgenommen", "abgleich"),
|
||||
|
||||
// Hosts, Gruppen, Kunden (#008, #010)
|
||||
"host.angelegt": a("Host angelegt", "hosts"),
|
||||
"host.geaendert": a("Host geändert", "hosts"),
|
||||
"host.archiviert": a("Host archiviert", "hosts"),
|
||||
"host.bootstrap.angefordert": a("Einrichtung angefordert", "hosts", false),
|
||||
"host.bootstrap": a("Host eingerichtet (Bootstrap)", "hosts"),
|
||||
"host.bootstrap.geprueft": a("Einrichtung geprüft", "hosts"),
|
||||
"hostgruppe.angelegt": a("Hostgruppe angelegt", "hosts"),
|
||||
"hostgruppe.geaendert": a("Hostgruppe geändert", "hosts"),
|
||||
"kunde.angelegt": a("Kunde angelegt", "hosts"),
|
||||
verbindungstest: a("Verbindung geprüft", "hosts", false),
|
||||
|
||||
// Personen und Vorlagen (#013, #019)
|
||||
"person.angelegt": a("Person angelegt", "stammdaten"),
|
||||
"person.geaendert": a("Person geändert", "stammdaten"),
|
||||
"person.archiviert": a("Person archiviert", "stammdaten"),
|
||||
"person.verknuepft": a("Person mit Konto verknüpft", "stammdaten"),
|
||||
"vorlage.angelegt": a("Vorlage angelegt", "stammdaten"),
|
||||
"vorlage.geaendert": a("Vorlage geändert", "stammdaten"),
|
||||
"vorlage.archiviert": a("Vorlage archiviert", "stammdaten"),
|
||||
|
||||
// Protokoll selbst (#027)
|
||||
"protokoll.exportiert": a("Protokoll exportiert (CSV)", "protokoll", false),
|
||||
"protokoll.kette_beschaedigt": a("Hash-Kette beschädigt gefunden", "protokoll", false),
|
||||
};
|
||||
|
||||
/** Text zur Aktion; unbekannte (etwa aus aelteren Versionen) bleiben technisch lesbar. */
|
||||
export function aktionText(aktion: string): string {
|
||||
return AKTIONEN[aktion]?.text ?? aktion;
|
||||
}
|
||||
|
||||
/** Alle bekannten Aktionen einer Gruppe – Grundlage fuer den Filter `aktion IN (…)`. */
|
||||
export function aktionenDerGruppe(gruppe: Aktionsgruppe): string[] {
|
||||
return Object.entries(AKTIONEN)
|
||||
.filter(([, b]) => b.gruppe === gruppe)
|
||||
.map(([aktion]) => aktion);
|
||||
}
|
||||
|
||||
export type AktionsTon = "ok" | "gefahr" | "neutral";
|
||||
|
||||
/** Farbe in der Liste: Fehlschlag und Verweigerung immer --danger, Aenderungen --ok. */
|
||||
export function aktionTon(aktion: string, ergebnis: "ERFOLG" | "FEHLER" | "VERWEIGERT"): AktionsTon {
|
||||
if (ergebnis !== "ERFOLG") return "gefahr";
|
||||
return AKTIONEN[aktion]?.aendert ? "ok" : "neutral";
|
||||
}
|
||||
|
||||
export function istAktionsgruppe(wert: string): wert is Aktionsgruppe {
|
||||
return Object.hasOwn(AKTIONSGRUPPEN, wert);
|
||||
}
|
||||
Reference in New Issue
Block a user