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:
parent
0ee9e4dd38
commit
02637c1f32
3 changed files with 73 additions and 10 deletions
|
|
@ -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`,
|
||||
`estimate-balance-format.ts`, `room-area.ts` – check for ReDoS when inputs can be long;
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
(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>
|
||||
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
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess # nosec B404 - Tests starten ist der Zweck des Skripts
|
||||
import sys
|
||||
import tomllib
|
||||
|
|
@ -53,16 +58,64 @@ def _write(target: Path, text: str) -> None:
|
|||
pyc.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _failed_lines(stdout: str) -> list[str]:
|
||||
# pytest: "FAILED ...", node:test: "✖ <Testname>" (ohne Suiten und Zusammenfassung)
|
||||
def _tap_fail(stripped: str) -> bool:
|
||||
# "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 = []
|
||||
for line in stdout.splitlines():
|
||||
node_fail = line.startswith(" ✖") and "failing tests" not in line
|
||||
if line.startswith("FAILED") or node_fail:
|
||||
lines.append(line.strip())
|
||||
for line in output.splitlines():
|
||||
stripped = line.strip()
|
||||
node_fail = stripped.startswith("✖") and "failing tests" not in line
|
||||
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
|
||||
|
||||
|
||||
# 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:
|
||||
base = config_path.parent
|
||||
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)
|
||||
finally:
|
||||
_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:
|
||||
print(f"[ÜBERLEBT] {name}: Tests bleiben grün")
|
||||
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:
|
||||
failed = _failed_lines(result.stdout)
|
||||
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"[GETÖTET] {name}: {failed[0]}")
|
||||
|
||||
print(f"\n{len(mutations) - len(survivors)}/{len(mutations)} Mutationen erkannt.")
|
||||
return 1 if survivors else 0
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ describe("Eingaben", () => {
|
|||
});
|
||||
|
||||
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 failures: string[] = [];
|
||||
for (const c of config.invalidInputCases) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue