diff --git a/docs/issues/005-protokoll.md b/docs/issues/005-protokoll.md index c29388e..7262a09 100644 --- a/docs/issues/005-protokoll.md +++ b/docs/issues/005-protokoll.md @@ -1,23 +1,27 @@ # #005 Revisionssicheres Protokoll -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.1.0 **Labels:** sicherheit -**Geschätzt:** 2 h **Tatsächlich:** – +**Geschätzt:** 2 h **Tatsächlich:** 0 h 10 min ## Ziel Jede Aktion landet im Protokoll; Einträge sind unveränderlich und über eine Hash-Kette verbunden. ## Aufgaben -- [ ] Modell `Protokoll` mit Akteur, Aktion, Host, Zielbenutzer, Fingerprint, Diff, Ergebnis -- [ ] DB-Trigger verbietet UPDATE/DELETE -- [ ] Hash-Kette (SHA-256 über Vorgänger + Inhalt), Prüffunktion -- [ ] Redaction: Private Keys/Passwörter können nicht hineingeraten +- [x] Modell `Protokoll` mit Akteur, Aktion, Host, Zielbenutzer, Fingerprint, Diff, Ergebnis +- [x] DB-Trigger verbietet UPDATE/DELETE +- [x] Hash-Kette (SHA-256 über Vorgänger + Inhalt), Prüffunktion +- [x] Redaction: Private Keys/Passwörter können nicht hineingeraten ## Akzeptanzkriterien -- [ ] Test: UPDATE/DELETE schlägt fehl -- [ ] Test: manipulierte Zeile wird von der Prüfung gefunden +- [x] Test: UPDATE/DELETE schlägt fehl +- [x] Test: manipulierte Zeile wird von der Prüfung gefunden ## Abhängigkeiten #003 ## Notizen / Entscheidungen +- Modell, Trigger und Hash-Kette entstanden schon mit #003 (Datenbankregeln); hier kommen Service, Schwärzen und Prüffunktion dazu. +- **Diff und Details werden geschwärzt**, bevor sie in die Datenbank gehen (`schwaerzen()` aus #006). +- **„privat“ ist kein Schwärz-Stamm mehr:** `privateVerwendungen`, `privatePfad` usw. sind Angaben, die das Protokoll gerade braucht. Geschwärzt werden nur Felder, die den Schlüssel selbst tragen (`privateKey*`, `privatVerschluesselt`, `dekVerschluesselt` …) – und jeder PEM-Block im Inhalt. +- Ansicht, CSV-Export und Syslog folgen mit #027. diff --git a/src/lib/protokoll-schwaerzen.ts b/src/lib/protokoll-schwaerzen.ts index 2fffbf7..aac1645 100644 --- a/src/lib/protokoll-schwaerzen.ts +++ b/src/lib/protokoll-schwaerzen.ts @@ -17,9 +17,17 @@ export const GESCHWAERZT = "[geschwärzt]"; * Kleinschreibung und ohne `_`/`-`, und zwar irgendwo im Namen: `clientSecret` und * `x-api-token` sollen genauso greifen wie `privateKeyOpenssh` oder `privatVerschluesselt`. * Lieber einmal zu viel schwaerzen als einen Key ins Log schreiben. + * + * „privat“ allein ist bewusst kein Stamm (#005): `privateVerwendungen`, `privatePfad` oder + * `privatGeloeschtAm` sind harmlose Angaben, die im Protokoll gerade gebraucht werden. + * Gemeint sind nur Felder, die den Schluessel selbst tragen. */ const GEHEIME_WORTSTAEMME = [ - "privat", + "privatekey", + "privatkey", + "privatschluessel", + "privatverschluesselt", + "dekverschluesselt", "passwort", "password", "passphrase", diff --git a/src/server/services/protokoll.ts b/src/server/services/protokoll.ts index 704151e..f6cd9fd 100644 --- a/src/server/services/protokoll.ts +++ b/src/server/services/protokoll.ts @@ -1,6 +1,7 @@ import type { Prisma } from "@/generated/prisma/client"; import type { ProtokollErgebnis } from "@/generated/prisma/enums"; import { prisma } from "@/lib/db"; +import { schwaerzen } from "@/lib/protokoll-schwaerzen"; /** * Schreibt einen Eintrag ins revisionssichere Protokoll (#005). @@ -34,8 +35,18 @@ export async function protokollieren( hostName: eintrag.hostName ?? null, zielbenutzer: eintrag.zielbenutzer ?? null, fingerprint: eintrag.fingerprint ?? null, - diff: eintrag.diff ?? null, - details: (eintrag.details as Prisma.InputJsonValue | undefined) ?? undefined, + // Auch der Diff: er stammt aus Dateien auf den Hosts, und eine authorized_keys-Datei + // kann versehentlich einen eingefuegten Private Key enthalten. + diff: eintrag.diff ? (schwaerzen(eintrag.diff) as string) : null, + details: eintrag.details ? (schwaerzen(eintrag.details) as Prisma.InputJsonValue) : undefined, }, }); } + +/** Prueft die Hash-Kette in der Datenbank; liefert die erste fehlerhafte ID oder null. */ +export async function protokollKettePruefen(): Promise { + const [zeile] = await prisma.$queryRaw< + { ergebnis: bigint | null }[] + >`SELECT protokoll_kette_pruefen() AS ergebnis`; + return zeile?.ergebnis ?? null; +} diff --git a/tests/krypto/protokoll-schwaerzen.test.ts b/tests/krypto/protokoll-schwaerzen.test.ts index b740216..40b3690 100644 --- a/tests/krypto/protokoll-schwaerzen.test.ts +++ b/tests/krypto/protokoll-schwaerzen.test.ts @@ -96,3 +96,19 @@ describe("schwaerzen", () => { expect(schwaerzen({ x: geteilt, y: geteilt })).toEqual({ x: { a: 1 }, y: { a: 1 } }); }); }); + +describe("harmlose Felder mit privat im Namen (#005)", () => { + it("bleiben lesbar", () => { + expect( + schwaerzen({ + privateVerwendungen: 2, + privatePfad: "~/.ssh/id_ed25519_x", + privatGeloeschtAm: "2026-10-12", + }), + ).toEqual({ + privateVerwendungen: 2, + privatePfad: "~/.ssh/id_ed25519_x", + privatGeloeschtAm: "2026-10-12", + }); + }); +}); diff --git a/tests/protokoll.test.ts b/tests/protokoll.test.ts new file mode 100644 index 0000000..233854e --- /dev/null +++ b/tests/protokoll.test.ts @@ -0,0 +1,49 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { prisma } from "@/lib/db"; +import { protokollieren, protokollKettePruefen } from "@/server/services/protokoll"; +import { datenbankLeeren } from "./hilfen/db"; + +/** Protokoll-Service (#005): schwaerzt vor dem Schreiben und laesst die Kette pruefen. */ + +const KEY = + "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----"; + +beforeEach(async () => { + await datenbankLeeren(); +}); + +describe("protokollieren", () => { + it("schreibt Akteur, Aktion und Ergebnis", async () => { + await protokollieren({ + akteur: { id: "b1", name: "Florian" }, + aktion: "schluessel.erzeugt", + ergebnis: "ERFOLG", + hostName: "srv-pve01", + fingerprint: "SHA256:abc", + }); + const [eintrag] = await prisma.protokoll.findMany(); + expect(eintrag).toMatchObject({ + akteurName: "Florian", + aktion: "schluessel.erzeugt", + hostName: "srv-pve01", + }); + expect(await protokollKettePruefen()).toBeNull(); + }); + + it("laesst keinen Private Key und kein Passwort hinein", async () => { + await protokollieren({ + akteur: "System", + aktion: "test", + ergebnis: "FEHLER", + diff: `+ ${KEY}`, + details: { passwort: "geheim123", fehler: `Parser scheiterte an ${KEY}`, privatePfad: "~/.ssh/id_x" }, + }); + const [eintrag] = await prisma.protokoll.findMany(); + const text = JSON.stringify(eintrag, (_, wert: unknown) => + typeof wert === "bigint" ? String(wert) : wert, + ); + expect(text).not.toContain("PRIVATE KEY"); + expect(text).not.toContain("geheim123"); + expect(text).toContain("~/.ssh/id_x"); + }); +});