Files
craftvia/scripts/test-framework-dryrun.ts
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +02:00

119 lines
6.4 KiB
TypeScript

// Abnahmetest der Mehr-Framework-Fähigkeit (ISO 27001 neben TISAX) — reiner Trockenlauf.
//
// Prüft die Zusagen aus docs/UEBERGABE-framework-iso27001.md, ohne etwas zu schreiben:
// 1. Paketebene: beide Mappings lesen denselben Dokumentensatz; kein ISO-Umsetzungstext
// ist leer; die Anforderungs-IDs beider Frameworks überschneiden sich nicht.
// 2. Falle 1.1: Ein ISO-Import in einen Mandanten mit TISAX archiviert KEINE
// TISAX-Anforderung (`report.requirements.archived === 0`).
// 3. Geteilte Inhalte (Dokumente, Variablen, Baseline, Nachweisregister) werden bei einem
// Mandanten, der das Paket bereits hat, nicht doppelt angelegt.
// 4. Der Trockenlauf hinterlässt keine Spuren: Zählstände vor und nach dem Lauf identisch.
//
// Lauf: npx tsx scripts/test-framework-dryrun.ts
// Nutzt die lokale Postgres-DB; .env liegt im Worktree. Ohne Mandanten läuft nur Teil 1.
import "dotenv/config";
import { join } from "node:path";
import { prisma } from "../src/server/db";
import { parsePackageFiles, reconcilePackage } from "../prisma/import-policies";
const SEED = join(process.cwd(), "seed", "isms-vorlagenpaket-v2");
let failed = 0;
function check(ok: boolean, label: string, detail = "") {
if (!ok) failed++;
console.log(` ${ok ? "✓" : "✗"} ${label}${detail ? " — " + detail : ""}`);
}
async function counts(tenantId: string) {
return {
tisax: await prisma.policyRequirement.count({ where: { tenantId, framework: "TISAX", archivedAt: null } }),
tisaxArch: await prisma.policyRequirement.count({ where: { tenantId, framework: "TISAX", archivedAt: { not: null } } }),
iso: await prisma.policyRequirement.count({ where: { tenantId, framework: "ISO_27001", archivedAt: null } }),
docs: await prisma.policyDocument.count({ where: { tenantId, archivedAt: null } }),
};
}
async function main() {
console.log("1. Paketebene");
const iso = parsePackageFiles(SEED, "mapping-iso.json");
const tis = parsePackageFiles(SEED, "mapping.json");
const isoCodes = iso.documents.map((d) => d.code).sort();
const tisCodes = tis.documents.map((d) => d.code).sort();
check(JSON.stringify(isoCodes) === JSON.stringify(tisCodes),
"beide Mappings lesen denselben Dokumentensatz", `${isoCodes.length} Dokumente`);
check(iso.requirements.length === 120,
"ISO-Mapping enthält 120 Anforderungen", `${iso.requirements.length}`);
check(iso.requirements.filter((r) => !r.implementation.trim()).length === 0,
"kein ISO-Umsetzungstext ist leer");
check(iso.requirements.every((r) => !!r.control),
"jede ISO-Anforderung trägt eine Control-Referenz");
const overlap = iso.requirements.map((r) => r.reqId).filter((id) => tis.requirements.some((t) => t.reqId === id));
check(overlap.length === 0, "Anforderungs-IDs beider Frameworks überschneiden sich nicht",
overlap.length ? overlap.slice(0, 3).join(", ") : "");
// Englische Fassung: gleiche Struktur, übersetzte Texte.
const SEED_EN = join(process.cwd(), "seed", "isms-vorlagenpaket-v2-en");
const isoEn = parsePackageFiles(SEED_EN, "mapping-iso.json");
check(isoEn.requirements.length === iso.requirements.length,
"EN-Fassung enthält gleich viele ISO-Anforderungen", `${isoEn.requirements.length}`);
check(JSON.stringify(isoEn.requirements.map((r) => r.reqId).sort())
=== JSON.stringify(iso.requirements.map((r) => r.reqId).sort()),
"EN- und DE-Fassung haben identische Anforderungs-IDs");
check(isoEn.requirements.filter((r) => !r.implementation.trim()).length === 0,
"kein EN-Umsetzungstext ist leer");
check(isoEn.documents.length === iso.documents.length,
"EN-Fassung hat denselben Dokumentenumfang", `${isoEn.documents.length}`);
const tenants = await prisma.tenant.findMany({ select: { id: true, name: true, slug: true } });
if (!tenants.length) {
console.log("\n(keine Mandanten in der DB — Teil 2 übersprungen)");
return;
}
for (const t of tenants) {
console.log(`\n2. Trockenlauf ISO_27001 gegen „${t.name}" (${t.slug})`);
const before = await counts(t.id);
console.log(` vorher: TISAX=${before.tisax} (archiviert ${before.tisaxArch}) · ISO=${before.iso} · Dokumente=${before.docs}`);
const { report } = await reconcilePackage(prisma, t.id, iso, { dryRun: true, framework: "ISO_27001" });
const r = report.requirements, d = report.documents;
console.log(` Anforderungen: +${r.added} ~${r.updated} =${r.unchanged} archiviert=${r.archived}`);
console.log(` Dokumente: +${d.added} ~${d.updated} =${d.unchanged} reaktiviert=${d.reactivated} archiviert=${d.archived}`);
check(r.archived === 0, "Falle 1.1 — keine fremde Anforderung archiviert", `archived=${r.archived}`);
check(r.added + r.unchanged + r.updated === 120, "alle 120 ISO-Anforderungen erfasst");
check(d.archived === 0, "kein geteiltes Dokument wird stillgelegt", `archived=${d.archived}`);
check(report.variables.obsolete === 0 && report.baseline.obsolete === 0 && report.evidence.obsolete === 0,
"geteilte Inhalte verlieren nichts (Variablen, Baseline, Nachweisregister)");
// Mandant hat das Paket bereits: jedes vorhandene Paket-Dokument muss wiedererkannt
// werden; angelegt wird nur, was im Mandanten wirklich fehlt.
const vorhanden = await prisma.policyDocument.findMany({
where: { tenantId: t.id, code: { in: iso.documents.map((x) => x.code) } },
select: { code: true },
});
// Ein vorhandenes Dokument zählt je nach Zustand als unchanged, updated ODER
// reactivated (wenn es beim Mandanten stillgelegt war) — alle drei sind „wiedererkannt".
const wiedererkannt = d.unchanged + d.updated + d.reactivated;
check(wiedererkannt === vorhanden.length,
"jedes vorhandene Paket-Dokument wird wiedererkannt",
`vorhanden=${vorhanden.length} unchanged=${d.unchanged} updated=${d.updated} reaktiviert=${d.reactivated}`);
check(d.added === iso.documents.length - vorhanden.length,
"angelegt wird nur, was im Mandanten fehlt",
`added=${d.added} erwartet=${iso.documents.length - vorhanden.length}`);
const after = await counts(t.id);
check(JSON.stringify(before) === JSON.stringify(after),
"Trockenlauf hat nichts geschrieben", JSON.stringify(after));
}
}
main()
.catch((e) => { console.error(e); failed++; })
.finally(async () => {
await prisma.$disconnect();
console.log(`\n${failed === 0 ? "OK — alle Prüfungen bestanden" : `FEHLGESCHLAGEN — ${failed} Prüfung(en)`}`);
process.exit(failed === 0 ? 0 : 1);
});