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`,
|
`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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue