import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { prisma } from "@/lib/db"; import { schluesselpaarErzeugen } from "@/lib/ssh/openssh"; import { zieleVonPrivatVerwendungen, zieleVonVerwendungen } from "@/server/services/soll"; import { privateVerwendungAnlegen, publicVerwendungAnlegen } from "@/server/services/verwendungen"; import { keyUebernehmen } from "@/server/services/uebernahme"; import { deploymentKeySicherstellen, schluesselpaarVersiegeltErzeugen } from "@/worker/geheimnisse"; import { datenbankLeeren } from "../hilfen/db"; import { eingerichteterHost, masterKeyAnlegen, vorgangLaufen } from "./hilfen/host"; import { befehlAlsRoot, zielA, zielBereit } from "./hilfen/ziele"; /** Abgleich und Drift-Erkennung gegen ein echtes sshd (#021). */ const AKTEUR = { id: "test", name: "Test" }; let aufraeumen: () => void; beforeAll(async () => { await zielBereit(zielA); aufraeumen = masterKeyAnlegen(); }); afterAll(() => aufraeumen()); beforeEach(async () => { await datenbankLeeren(); await deploymentKeySicherstellen(); await befehlAlsRoot( zielA, "userdel -r sshkv 2>/dev/null; rm -f /etc/sudoers.d/sshkv; rm -rf /root/.ssh; true", ); }); async function ausgerollterHost() { const host = await eingerichteterHost(zielA, "ziel-a"); const paar = await schluesselpaarErzeugen("ED25519", { kommentar: "sshkv:abgleich" }); const schluessel = await prisma.schluessel.create({ data: { name: "Abgleich", slug: "abgleich", typ: "ED25519", kategorie: "DIENST", publicKey: paar.publicKeyZeile, fingerprint: paar.fingerprint, kommentar: "sshkv:abgleich", }, }); await publicVerwendungAnlegen( { schluesselId: schluessel.id, hostId: host.id, zielbenutzer: "root" }, AKTEUR, ); const trockenlauf = await vorgangLaufen("TROCKENLAUF", { eingabe: { ziele: await zieleVonVerwendungen({ schluesselId: schluessel.id }) }, }); await vorgangLaufen("AUSROLLEN", { eingabe: { trockenlaufId: trockenlauf.id } }); return { host, schluessel }; } const abgleichen = (hostId: string) => vorgangLaufen("ABGLEICH", { hostId }); // Postgres sortiert Enums nach Deklaration; fuer lesbare Erwartungen alphabetisch in JS. const offen = async (hostId: string) => (await prisma.abweichung.findMany({ where: { hostId, behobenAm: null } })).sort((a, b) => a.art.localeCompare(b.art), ); describe("Abgleich", () => { it("ist nach dem Ausrollen synchron und haelt den Ist-Zustand je Benutzer fest", async () => { const { host } = await ausgerollterHost(); const vorgang = await abgleichen(host.id); expect(vorgang.fehler).toBeNull(); expect(await offen(host.id)).toEqual([]); const aktuell = await prisma.host.findUniqueOrThrow({ where: { id: host.id } }); expect(aktuell.driftZustand).toBe("SYNCHRON"); expect(aktuell.letzterAbgleichAm).not.toBeNull(); const beobachtet = await prisma.beobachtung.findMany({ where: { hostId: host.id }, orderBy: { zielbenutzer: "asc" }, }); expect(beobachtet.map((b) => b.zielbenutzer)).toEqual(["root", "sshkv"]); expect(JSON.stringify(beobachtet[0]?.eintraege)).toContain('"herkunft":"verwaltet"'); }); it("findet fremde Keys, fehlende Keys und falsche Rechte – und schliesst behobene Befunde", async () => { const { host } = await ausgerollterHost(); const fremd = await schluesselpaarErzeugen("ED25519", { kommentar: "fremd@laptop" }); await befehlAlsRoot( zielA, `echo '${fremd.publicKeyZeile}' >> /root/.ssh/authorized_keys && sed -i '/sshkv:abgleich/d' /root/.ssh/authorized_keys && chmod 777 /root/.ssh`, ); await abgleichen(host.id); const arten = (await offen(host.id)).map((a) => a.art); expect(arten).toEqual(["FEHLENDER_SCHLUESSEL", "RECHTE", "UNBEKANNTER_SCHLUESSEL"]); expect((await prisma.host.findUniqueOrThrow({ where: { id: host.id } })).driftZustand).toBe("DRIFT"); expect((await prisma.publicVerwendung.findFirstOrThrow()).zustand).toBe("ABWEICHUNG"); // Zweiter Lauf ohne Aenderung: keine doppelten Befunde. await abgleichen(host.id); expect(await offen(host.id)).toHaveLength(3); // Von Hand bereinigen: fremden Key raus, Rechte zurueck – der fehlende Key bleibt Befund. await befehlAlsRoot(zielA, "sed -i '/fremd@laptop/d' /root/.ssh/authorized_keys && chmod 700 /root/.ssh"); await abgleichen(host.id); expect((await offen(host.id)).map((a) => a.art)).toEqual(["FEHLENDER_SCHLUESSEL"]); }); it("bemerkt einen veraenderten Private Key", async () => { const host = await eingerichteterHost(zielA, "ziel-a"); const neu = await schluesselpaarVersiegeltErzeugen("ED25519", "sshkv:privat"); const schluessel = await prisma.schluessel.create({ data: { id: neu.id, name: "Privat", slug: "privat", typ: "ED25519", kategorie: "DIENST", publicKey: neu.publicKeyZeile, fingerprint: neu.fingerprint, kommentar: "sshkv:privat", ...neu.versiegelt, }, }); await privateVerwendungAnlegen( { schluesselId: schluessel.id, hostId: host.id, zielbenutzer: "root", pfad: "~/.ssh/id_privat" }, AKTEUR, ); const trockenlauf = await vorgangLaufen("TROCKENLAUF", { eingabe: { ziele: await zieleVonPrivatVerwendungen({ schluesselId: schluessel.id }) }, }); await vorgangLaufen("AUSROLLEN", { eingabe: { trockenlaufId: trockenlauf.id } }); await abgleichen(host.id); expect(await offen(host.id)).toEqual([]); await befehlAlsRoot(zielA, "echo kaputt > /root/.ssh/id_privat"); await abgleichen(host.id); expect((await offen(host.id)).map((a) => a.art)).toEqual(["PRIVATE_KEY_FEHLT"]); expect((await prisma.privateVerwendung.findFirstOrThrow()).zustand).toBe("ABWEICHUNG"); }); it("uebernimmt einen fremden Key in den verwalteten Block (#022)", async () => { const { host } = await ausgerollterHost(); const fremd = await schluesselpaarErzeugen("ED25519", { kommentar: "anna@laptop" }); await befehlAlsRoot(zielA, `sed -i '1i no-pty ${fremd.publicKeyZeile}' /root/.ssh/authorized_keys`); await abgleichen(host.id); expect((await offen(host.id)).map((a) => a.art)).toEqual(["UNBEKANNTER_SCHLUESSEL"]); const admin = await prisma.benutzer.create({ data: { id: "test", name: "Test", rolle: "ADMIN" } }); const { schluesselId } = await keyUebernehmen( { hostId: host.id, zielbenutzer: "root", fingerprint: fremd.fingerprint, name: "Laptop Anna", kategorie: "PERSON", personId: null, verwalten: true, }, { id: admin.id, name: admin.name }, ); const trockenlauf = await vorgangLaufen("TROCKENLAUF", { eingabe: { ziele: await zieleVonVerwendungen({ schluesselId }) }, }); await vorgangLaufen("AUSROLLEN", { eingabe: { trockenlaufId: trockenlauf.id } }); const datei = (await befehlAlsRoot(zielA, "cat /root/.ssh/authorized_keys")).stdout; expect(datei.startsWith("# BEGIN SSHKV MANAGED")).toBe(true); expect(datei.match(/anna@laptop/g)).toHaveLength(1); expect(datei).toContain(`no-pty ${fremd.publicKeyZeile}`); await abgleichen(host.id); expect(await offen(host.id)).toEqual([]); }); });