Files
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

103 lines
6.0 KiB
TypeScript

// Akzeptanztest der Regel-Engine (Story F3/B2). Deterministische Assertions gegen
// C2 §5 (Q-FEAT-01…10) und §8 (Aufgaben-Trigger). Kein DB-Zugriff, reine Logik.
// Lauf: npx tsx scripts/test-rules.ts (Exit 1 bei Fehler).
import { evaluateRules, makeContext } from "../src/lib/rules/engine";
import { ALL_RULES, FEATURE_RULES, Q } from "../src/lib/rules/feature-rules";
import { triggerById } from "../src/lib/task-triggers";
import type { Answer } from "../src/lib/rules/dsl";
let failures = 0;
const ok = (cond: boolean, msg: string) => {
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
if (!cond) failures++;
};
const evalAnswers = (answers: Record<string, Answer>, pruefziele?: string[]) =>
evaluateRules(ALL_RULES, makeContext({ answers, ...(pruefziele ? { pruefziele } : {}) }));
// — Q-FEAT-02 Cloud —
{
const on = evalAnswers({ [Q.CLOUD]: true });
ok(on.flags.FLAG_CLOUD_USED === true, "Q-FEAT-02=ja → FLAG_CLOUD_USED");
ok(["5.3.2", "5.3.4", "6.1.3"].every((c) => on.controls.includes(c)), "Q-FEAT-02=ja → Controls 5.3.2/5.3.4/6.1.3");
ok(on.risks.includes("R-CLOUD"), "Q-FEAT-02=ja → Risiko R-CLOUD");
ok(on.matched.includes("feat02-cloud"), "Q-FEAT-02=ja → Regel feat02-cloud ausgelöst");
const off = evalAnswers({ [Q.CLOUD]: false });
ok(off.flags.FLAG_CLOUD_USED !== true, "Q-FEAT-02=nein → kein FLAG_CLOUD_USED");
ok(!off.matched.includes("feat02-cloud"), "Q-FEAT-02=nein → feat02-cloud NICHT ausgelöst");
ok(!off.controls.includes("5.3.2"), "Q-FEAT-02=nein → Control 5.3.2 nicht im Scope");
}
// — A2-1: Schutzbedarf kommt zentral aus dem Assessment-Level (Seed), NICHT aus dem Fragebogen —
{
// Ohne AL-Seed setzt keine Fragebogen-Antwort HIGH/VERY_HIGH.
const noSeed = evalAnswers({ [Q.CLOUD]: true, [Q.EXTERNAL_IT]: true });
ok(noSeed.flags.FLAG_HIGH_PROTECTION !== true && noSeed.flags.FLAG_VERY_HIGH_PROTECTION !== true, "Fragebogen setzt HIGH/VERY_HIGH nicht");
ok(noSeed.flags.FLAG_ELEVATED_PROTECTION === false, "ohne AL-Seed → ELEVATED aus");
// AL2-Seed → HIGH an, VERY_HIGH aus; AL3-Seed → beide an; ELEVATED jeweils abgeleitet.
const al2 = evaluateRules(ALL_RULES, makeContext({ flags: { FLAG_HIGH_PROTECTION: true, FLAG_VERY_HIGH_PROTECTION: false } }));
ok(al2.flags.FLAG_HIGH_PROTECTION === true && al2.flags.FLAG_VERY_HIGH_PROTECTION !== true, "AL2-Seed → HIGH an, VERY_HIGH aus");
ok(al2.flags.FLAG_ELEVATED_PROTECTION === true, "AL2-Seed → ELEVATED abgeleitet an");
const al3 = evaluateRules(ALL_RULES, makeContext({ flags: { FLAG_HIGH_PROTECTION: true, FLAG_VERY_HIGH_PROTECTION: true } }));
ok(al3.flags.FLAG_VERY_HIGH_PROTECTION === true && al3.flags.FLAG_ELEVATED_PROTECTION === true, "AL3-Seed → VERY_HIGH + ELEVATED an");
}
// — Q-FEAT-09 externe IT (Control + Risiko + Aufgabe) —
{
const r = evalAnswers({ [Q.EXTERNAL_IT]: true });
ok(r.flags.FLAG_EXTERNAL_IT === true, "Q-FEAT-09=ja → FLAG_EXTERNAL_IT");
ok(r.controls.includes("6.1.1") && r.controls.includes("6.1.3"), "Q-FEAT-09=ja → Controls 6.1.1/6.1.3");
ok(r.risks.includes("R-SUP"), "Q-FEAT-09=ja → Risiko R-SUP");
ok(r.tasks.includes("external_it_yes"), "Q-FEAT-09=ja → Aufgabe external_it_yes");
}
// — C2 §8 Aufgaben-Trigger —
{
const cloud = evalAnswers({ [Q.CLOUD]: true });
ok(cloud.tasks.includes("cloud_ai_without_approval"), "Cloud=ja → Aufgabe cloud_ai_without_approval");
const ai = evalAnswers({ [Q.AI]: true });
ok(ai.tasks.includes("cloud_ai_without_approval"), "KI=ja → Aufgabe cloud_ai_without_approval");
const neither = evalAnswers({ [Q.CLOUD]: false, [Q.AI]: false });
ok(!neither.tasks.includes("cloud_ai_without_approval"), "Cloud/KI=nein → keine Freigabeverfahren-Aufgabe");
const isb = evalAnswers({ [Q.ISB_BENANNT]: false });
ok(isb.tasks.includes("isb_not_named"), "ISB nicht benannt → Aufgabe isb_not_named");
const restore = evalAnswers({ [Q.RESTORE_GETESTET]: false });
ok(restore.tasks.includes("no_restore_test"), "kein Restore-Test → Aufgabe no_restore_test");
}
// — Determinismus: gleiche Eingabe → gleiches Ergebnis —
{
const a = evalAnswers({ [Q.CLOUD]: true, [Q.EXTERNAL_IT]: true });
const b = evalAnswers({ [Q.CLOUD]: true, [Q.EXTERNAL_IT]: true });
ok(JSON.stringify(a) === JSON.stringify(b), "Determinismus: identische Auswertung bei gleicher Eingabe");
}
// — Integrität: alle referenzierten Trigger-IDs existieren im Katalog (B1) —
{
const taskIds = new Set(ALL_RULES.flatMap((r) => r.effect.tasks ?? []));
const allExist = [...taskIds].every((id) => triggerById(id) !== null);
ok(allExist, `alle Aufgaben-Trigger-IDs existieren in task-triggers.ts (${[...taskIds].join(", ")})`);
}
// — Abdeckung: jede Q-FEAT-02…10 hat eine Regel (Q-FEAT-01/Schutzbedarf ist zentral, A2-1) —
{
const covered = new Set(FEATURE_RULES.map((r) => r.source));
const missing = Array.from({ length: 9 }, (_, i) => `C2 §5 Q-FEAT-${String(i + 2).padStart(2, "0")}`).filter((s) => !covered.has(s));
ok(missing.length === 0, `alle Q-FEAT-02…10 durch Regeln abgedeckt${missing.length ? " — fehlt: " + missing.join(", ") : ""}`);
}
// — Follow-up: Prüfziele kommen aus WizardScope (Scoping), nicht aus dem Fragebogen —
{
const ds = evalAnswers({}, ["informationssicherheit", "datenschutz"]);
ok(ds.flags.FLAG_PERSONAL_DATA === true, "Prüfziel Datenschutz → FLAG_PERSONAL_DATA");
ok(ds.controls.includes("7.1.2") && ds.risks.includes("R-DSGVO"), "Prüfziel Datenschutz → Control 7.1.2 + Risiko R-DSGVO");
const proto = evalAnswers({}, ["informationssicherheit", "prototypenschutz"]);
ok(proto.flags.FLAG_PROTOTYPE_PROTECTION === true, "Prüfziel Prototypenschutz → FLAG_PROTOTYPE_PROTECTION");
const isOnly = evalAnswers({}, ["informationssicherheit"]);
ok(isOnly.flags.FLAG_PERSONAL_DATA !== true && isOnly.flags.FLAG_PROTOTYPE_PROTECTION !== true, "nur Informationssicherheit → Datenschutz/Prototyp-Flags aus");
}
console.log(failures === 0 ? "\nOK — alle Regel-Tests grün" : `\nPRUEFEN — ${failures} Fehler`);
process.exit(failures === 0 ? 0 : 1);