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>
103 lines
6.0 KiB
TypeScript
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);
|