chore(security): Baseline-Update 2026-10-27 (mutation_check, Testvorlage)

Aus cj/security-baseline a04c20b übernommen:
- scripts/security/mutation_check.py: Syntax-/Importfehler aus einer Mutation zählen nicht
  mehr als getötet ([UNGÜLTIG]); getötet nur bei echtem Testfehler (pytest, unittest,
  node:test spec und TAP), TODO-/SKIP-Tests sind kein Beleg.
- Sicherheitstest-Vorlage: ungültige Eingaben müssen konfiguriert sein (Leerlauf-Sperre).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-10-27 22:04:48 +01:00
parent 0ee9e4dd38
commit 02637c1f32
3 changed files with 73 additions and 10 deletions

View file

@ -818,6 +818,7 @@ Open security points (state at adoption, 2026-10-15; updated 2026-10-17):
`project-state-snapshot.model.ts`, `Sidebar.tsx`, `cable-sizing-modal.tsx`, `project-state-snapshot.model.ts`, `Sidebar.tsx`, `cable-sizing-modal.tsx`,
`estimate-balance-format.ts`, `room-area.ts` – check for ReDoS when inputs can be long; `estimate-balance-format.ts`, `room-area.ts` – check for ReDoS when inputs can be long;
several `react-hooks/exhaustive-deps` warnings in the editor. several `react-hooks/exhaustive-deps` warnings in the editor.
- Baseline-Stand 2026-10-27 (security-baseline a04c20b): `mutation_check.py` zählt Syntax-/Importfehler nicht mehr als getötet (`[UNGÜLTIG]`), Sicherheitstests verlangen konfigurierte ungültige Eingaben (Leerlauf-Sperre).
## Response Style for Codex ## Response Style for Codex

View file

@ -5,6 +5,10 @@ Baut gezielt je eine Schutzfunktion aus (Textersetzung), lässt die Tests laufen
erwartet ROT. Danach wird die Datei immer wiederhergestellt. Überlebt eine Mutation erwartet ROT. Danach wird die Datei immer wiederhergestellt. Überlebt eine Mutation
(Tests bleiben grün), prüfen die Tests diese Schutzfunktion nicht wirklich. (Tests bleiben grün), prüfen die Tests diese Schutzfunktion nicht wirklich.
Getötet ist eine Mutation nur, wenn ein Test fehlschlägt (pytest FAILED, unittest FAIL,
node:test ✖ bzw. TAP "not ok") und im Fehlerbericht kein Ladefehler steht (Syntax-/Importfehler, siehe
LOAD_ERRORS). Rot durch kaputten Code zählt als UNGÜLTIG.
Aufruf: mutation_check.py <mutations.toml> Aufruf: mutation_check.py <mutations.toml>
Exit: 0 = jede Mutation hat mindestens einen Test rot gemacht, 1 = sonst Exit: 0 = jede Mutation hat mindestens einen Test rot gemacht, 1 = sonst
@ -23,6 +27,7 @@ Format (Pfade relativ zur TOML-Datei):
from __future__ import annotations from __future__ import annotations
import os import os
import re
import subprocess # nosec B404 - Tests starten ist der Zweck des Skripts import subprocess # nosec B404 - Tests starten ist der Zweck des Skripts
import sys import sys
import tomllib import tomllib
@ -53,16 +58,64 @@ def _write(target: Path, text: str) -> None:
pyc.unlink(missing_ok=True) pyc.unlink(missing_ok=True)
def _failed_lines(stdout: str) -> list[str]: def _tap_fail(stripped: str) -> bool:
# pytest: "FAILED ...", node:test: "✖ <Testname>" (ohne Suiten und Zusammenfassung) # "not ok ... # TODO|SKIP": erwartet scheiternder Test (z. B. idorGap), macht den Lauf nicht rot
return stripped.startswith("not ok ") and not re.search(r"#\s*(TODO|SKIP)\b", stripped)
def _failed_lines(output: str) -> list[str]:
# pytest: "FAILED ...", unittest: "FAIL: ...", node:test: "✖ <Testname>" (ohne Zusammenfassung)
# bzw. im TAP-Format (ältere Node-Versionen, `tsx --test` ohne Terminal): "not ok <n> - <Testname>"
lines = [] lines = []
for line in stdout.splitlines(): for line in output.splitlines():
node_fail = line.startswith(" ✖") and "failing tests" not in line stripped = line.strip()
if line.startswith("FAILED") or node_fail: node_fail = stripped.startswith("✖") and "failing tests" not in line
lines.append(line.strip()) node_fail = node_fail or _tap_fail(stripped)
# "FAILED (errors=1)" ist die Zusammenfassung von unittest, kein fehlgeschlagener Test.
pytest_fail = line.startswith("FAILED ") and not line.startswith("FAILED (")
if pytest_fail or line.startswith("FAIL:") or node_fail:
lines.append(stripped)
return lines return lines
# Rot durch kaputten Code ist kein Testbeleg (2026-10-27): pytest meldet einen Ladefehler als
# ERROR ohne FAILED, node:test bricht alle Tests ab und markiert sie trotzdem mit ✖.
LOAD_ERRORS = (
"SyntaxError",
"IndentationError",
"TabError",
"ImportError",
"ModuleNotFoundError",
"error during collection",
"TransformError",
"ERR_MODULE_NOT_FOUND",
"Cannot find module",
"Cannot find package",
)
def _load_error(text: str) -> str | None:
return next((marker for marker in LOAD_ERRORS if marker in text), None)
def _failure_report(output: str, failed: list[str]) -> str:
# Nur der Fehlerbericht, nicht der ganze Lauf: Logs der App sind kein Ladefehler (Selftest
# 2026-10-27: Express loggt den JSON-SyntaxError von body-parser beim Test mit "{kaputt").
# node:test: Abschnitt "✖ failing tests:", TAP: die YAML-Blöcke unter "not ok" (App-Logs
# stehen dort als "# ..." außerhalb), pytest/unittest: die Fehlerzeilen selbst.
_, sep, tail = output.partition("✖ failing tests:")
if sep:
return tail
tap, in_block = [], False
for line in output.splitlines():
if _tap_fail(line.strip()):
in_block = True
if in_block:
tap.append(line)
in_block = line.strip() != "..."
return "\n".join(tap) if tap else "\n".join(failed)
def run(config_path: Path) -> int: def run(config_path: Path) -> int:
base = config_path.parent base = config_path.parent
config = tomllib.loads(config_path.read_text(encoding="utf-8")) config = tomllib.loads(config_path.read_text(encoding="utf-8"))
@ -95,14 +148,22 @@ def run(config_path: Path) -> int:
result = _run_tests(test_cmd, workdir) result = _run_tests(test_cmd, workdir)
finally: finally:
_write(target, original) _write(target, original)
output = result.stdout + result.stderr # unittest schreibt auf stderr
failed = _failed_lines(output)
load_error = _load_error(_failure_report(output, failed))
if result.returncode == 0: if result.returncode == 0:
print(f"[ÜBERLEBT] {name}: Tests bleiben grün") print(f"[ÜBERLEBT] {name}: Tests bleiben grün")
survivors.append(name) survivors.append(name)
elif load_error or not failed:
# Ohne Fehlerzeile (pytest: nur ERROR beim Sammeln) den Grund aus dem ganzen Lauf nennen.
load_error = load_error or _load_error(output)
why = f"Ladefehler ({load_error})" if load_error else "kein Testfehler erkennbar"
print(f"[UNGÜLTIG] {name}: Tests rot, aber {why}, kein Testbeleg.")
print(" Mutation so ändern, dass der Code lauffähig bleibt:")
print(output[-800:])
survivors.append(name)
else: else:
failed = _failed_lines(result.stdout) print(f"[GETÖTET] {name}: {failed[0]}")
hint = "Tests rot, Ursache prüfen (Syntaxfehler?):\n"
reason = failed[0] if failed else hint + result.stdout[-800:]
print(f"[GETÖTET] {name}: {reason}")
print(f"\n{len(mutations) - len(survivors)}/{len(mutations)} Mutationen erkannt.") print(f"\n{len(mutations) - len(survivors)}/{len(mutations)} Mutationen erkannt.")
return 1 if survivors else 0 return 1 if survivors else 0

View file

@ -132,6 +132,7 @@ describe("Eingaben", () => {
}); });
it("ungültige Eingaben führen nie zu 5xx", async () => { it("ungültige Eingaben führen nie zu 5xx", async () => {
assert.ok(config.invalidInputCases.length > 0, "invalidInputCases konfigurieren");
const obj = await config.createOwnedObject(baseUrl, owner); const obj = await config.createOwnedObject(baseUrl, owner);
const failures: string[] = []; const failures: string[] = [];
for (const c of config.invalidInputCases) { for (const c of config.invalidInputCases) {