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>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user